Compare commits
152
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2458e7597b | ||
|
|
23a721f0a2 | ||
|
|
9bd6be5c6d | ||
|
|
dd6113c9d1 | ||
|
|
d39bcd9c55 | ||
|
|
823d7da4c1 | ||
|
|
7fff191c57 | ||
|
|
9e44085a69 | ||
|
|
5230b91b25 | ||
|
|
493b3d72e4 | ||
|
|
337590c450 | ||
|
|
8e1c7cfe89 | ||
|
|
c60cb6c329 | ||
|
|
acf1dd8500 | ||
|
|
3fb57044d1 | ||
|
|
46a76a778a | ||
|
|
a9a2c23736 | ||
|
|
ccde319937 | ||
|
|
0ed7fac5fb | ||
|
|
80b9cd1292 | ||
|
|
b6c1df41fb | ||
|
|
125af820d0 | ||
|
|
8167e90801 | ||
|
|
82ebf66cba | ||
|
|
883ed4d424 | ||
|
|
e6bf1754c3 | ||
|
|
bcb494d5d1 | ||
|
|
75c0c0a098 | ||
|
|
840d2694b4 | ||
|
|
abdc7b276a | ||
|
|
d4f6deb9ef | ||
|
|
502e85b903 | ||
|
|
ac1e2bfd49 | ||
|
|
b9b071c744 | ||
|
|
83186b6fed | ||
|
|
a3a239967f | ||
|
|
b4fd4bb257 | ||
|
|
eb009d5959 | ||
|
|
4a81bd0f50 | ||
|
|
bbc9142fc5 | ||
|
|
7413c2715c | ||
|
|
7579c3bb15 | ||
|
|
24683058fd | ||
|
|
bf81e3108c | ||
|
|
5ade90416e | ||
|
|
8f2a8086c0 | ||
|
|
38b70f7877 | ||
|
|
5e112a17a5 | ||
|
|
59a3e7e3cc | ||
|
|
2c93f065cb | ||
|
|
488d33c1ed | ||
|
|
de4660ac12 | ||
|
|
27ae341684 | ||
|
|
25b3846694 | ||
|
|
8e2f9f6544 | ||
|
|
76b5870f89 | ||
|
|
604891e793 | ||
|
|
5814df7eaa | ||
|
|
2509d03f42 | ||
|
|
a256df9823 | ||
|
|
af96ec5a30 | ||
|
|
55df80b80e | ||
|
|
7d11986a0a | ||
|
|
d75d90c53e | ||
|
|
35fead2eca | ||
|
|
9bb2efd3ef | ||
|
|
30ffcaa667 | ||
|
|
ba11455786 | ||
|
|
d69ba27f84 | ||
|
|
448b72d046 | ||
|
|
d96d89bcb8 | ||
|
|
7a013d4f40 | ||
|
|
e4597df3b9 | ||
|
|
2b014fcd75 | ||
|
|
c2bf6975f8 | ||
|
|
733e1f79ac | ||
|
|
2a6cbfd5fd | ||
|
|
6173b69a8b | ||
|
|
fc72cfe784 | ||
|
|
768c81cdfd | ||
|
|
bcea8ed593 | ||
|
|
f93bb1dd21 | ||
|
|
b92e8510f9 | ||
|
|
1b692ec7eb | ||
|
|
4c97e2e8bb | ||
|
|
b652198979 | ||
|
|
31c4a1d853 | ||
|
|
6afdb5c0e5 | ||
|
|
bdcf864678 | ||
|
|
0dd5039668 | ||
|
|
a0831eade1 | ||
|
|
f1dc9818b6 | ||
|
|
b52b7c6ded | ||
|
|
e03a41144a | ||
|
|
37bb07e7a3 | ||
|
|
78a6325b64 | ||
|
|
c96923d2c9 | ||
|
|
59742fbfee | ||
|
|
2938a25ec5 | ||
|
|
d163eb3888 | ||
|
|
75c29d4d1c | ||
|
|
e103fb1f93 | ||
|
|
bd79ff87cc | ||
|
|
ac21ec2f46 | ||
|
|
5bcf017c10 | ||
|
|
85d99198b5 | ||
|
|
7f183f7404 | ||
|
|
85284df725 | ||
|
|
87054ee983 | ||
|
|
81245c2548 | ||
|
|
6f82b321d8 | ||
|
|
f4593c6653 | ||
|
|
15902cf54d | ||
|
|
d1102c33ac | ||
|
|
aabbd3383c | ||
|
|
afb55cb7d4 | ||
|
|
ade794a937 | ||
|
|
34271a82ff | ||
|
|
b20a31098a | ||
|
|
b5a039e5ae | ||
|
|
986cc0a01c | ||
|
|
b20fd36c48 | ||
|
|
e4e6bf66e1 | ||
|
|
3ae27273c6 | ||
|
|
eefb3c43dd | ||
|
|
cc229e726e | ||
|
|
49408c00e9 | ||
|
|
76192fbced | ||
|
|
a1c76c79de | ||
|
|
db9e2b1aac | ||
|
|
45c4970d68 | ||
|
|
1d7a9309d6 | ||
|
|
f5ac98251e | ||
|
|
78239045ba | ||
|
|
d8b60875c4 | ||
|
|
48949a6e9d | ||
|
|
082a330ea3 | ||
|
|
adf7df0d5c | ||
|
|
a76ad48563 | ||
|
|
00f991162f | ||
|
|
d6cdd24fad | ||
|
|
c9473756df | ||
|
|
b5d0c56b4c | ||
|
|
96a2f5268c | ||
|
|
5083f9c9c2 | ||
|
|
e34df15ff5 | ||
|
|
26ec87803a | ||
|
|
037e8d4555 | ||
|
|
08a366c4dc | ||
|
|
670e1523e0 | ||
|
|
416f2964b5 | ||
|
|
e018e16898 |
@@ -17,6 +17,10 @@ jobs:
|
|||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
- uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "24"
|
||||||
|
|
||||||
- run: bun sst deploy --stage=${{ github.ref_name }}
|
- run: bun sst deploy --stage=${{ github.ref_name }}
|
||||||
env:
|
env:
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Check for duplicate issues
|
- name: Check for duplicate issues
|
||||||
env:
|
env:
|
||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
OPENCODE_PERMISSION: |
|
OPENCODE_PERMISSION: |
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -61,6 +61,13 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Publish
|
- name: Publish
|
||||||
run: |
|
run: |
|
||||||
./script/publish.ts
|
./script/publish.ts
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.head_ref || github.ref_name }}
|
||||||
|
|
||||||
- name: Setup Nix
|
- name: Setup Nix
|
||||||
uses: DeterminateSystems/nix-installer-action@v20
|
uses: DeterminateSystems/nix-installer-action@v20
|
||||||
@@ -79,6 +80,7 @@ jobs:
|
|||||||
git commit -m "Update Nix flake.lock and hashes"
|
git commit -m "Update Nix flake.lock and hashes"
|
||||||
|
|
||||||
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
||||||
|
git pull --rebase origin "$BRANCH"
|
||||||
git push origin HEAD:"$BRANCH"
|
git push origin HEAD:"$BRANCH"
|
||||||
|
|
||||||
summarize "committed $(git rev-parse --short HEAD)"
|
summarize "committed $(git rev-parse --short HEAD)"
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
"plugin": ["opencode-openai-codex-auth"],
|
"plugin": ["opencode-openai-codex-auth"],
|
||||||
|
"enterprise": {
|
||||||
|
"url": "http://localhost:3000",
|
||||||
|
},
|
||||||
"provider": {
|
"provider": {
|
||||||
"opencode": {
|
"opencode": {
|
||||||
"options": {
|
"options": {
|
||||||
|
|||||||
@@ -146,3 +146,7 @@
|
|||||||
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
|
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
|
||||||
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
|
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
|
||||||
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
|
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
|
||||||
|
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
|
||||||
|
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
|
||||||
|
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
|
||||||
|
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
|
||||||
|
|||||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1763618868,
|
"lastModified": 1763806073,
|
||||||
"narHash": "sha256-v5afmLjn/uyD9EQuPBn7nZuaZVV9r+JerayK/4wvdWA=",
|
"narHash": "sha256-FHsEKDvfWpzdADWj99z7vBk4D716Ujdyveo5+A048aI=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "a8d610af3f1a5fb71e23e08434d8d61a466fc942",
|
"rev": "878e468e02bfabeda08c79250f7ad583037f2227",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
+10
-4
@@ -97,8 +97,12 @@ export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint",
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
const ZEN_MODELS1 = new sst.Secret("ZEN_MODELS1")
|
const ZEN_MODELS = [
|
||||||
const ZEN_MODELS2 = new sst.Secret("ZEN_MODELS2")
|
new sst.Secret("ZEN_MODELS1"),
|
||||||
|
new sst.Secret("ZEN_MODELS2"),
|
||||||
|
new sst.Secret("ZEN_MODELS3"),
|
||||||
|
new sst.Secret("ZEN_MODELS4"),
|
||||||
|
]
|
||||||
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
||||||
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
||||||
properties: { value: auth.url.apply((url) => url!) },
|
properties: { value: auth.url.apply((url) => url!) },
|
||||||
@@ -112,6 +116,8 @@ const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
|
|||||||
// CONSOLE
|
// CONSOLE
|
||||||
////////////////
|
////////////////
|
||||||
|
|
||||||
|
const bucket = new sst.cloudflare.Bucket("ConsoleData")
|
||||||
|
|
||||||
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
|
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
|
||||||
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
||||||
|
|
||||||
@@ -128,15 +134,15 @@ new sst.cloudflare.x.SolidStart("Console", {
|
|||||||
domain,
|
domain,
|
||||||
path: "packages/console/app",
|
path: "packages/console/app",
|
||||||
link: [
|
link: [
|
||||||
|
bucket,
|
||||||
database,
|
database,
|
||||||
AUTH_API_URL,
|
AUTH_API_URL,
|
||||||
STRIPE_WEBHOOK_SECRET,
|
STRIPE_WEBHOOK_SECRET,
|
||||||
STRIPE_SECRET_KEY,
|
STRIPE_SECRET_KEY,
|
||||||
ZEN_MODELS1,
|
|
||||||
ZEN_MODELS2,
|
|
||||||
EMAILOCTOPUS_API_KEY,
|
EMAILOCTOPUS_API_KEY,
|
||||||
AWS_SES_ACCESS_KEY_ID,
|
AWS_SES_ACCESS_KEY_ID,
|
||||||
AWS_SES_SECRET_ACCESS_KEY,
|
AWS_SES_SECRET_ACCESS_KEY,
|
||||||
|
...ZEN_MODELS,
|
||||||
...($dev
|
...($dev
|
||||||
? [
|
? [
|
||||||
new sst.Secret("CLOUDFLARE_DEFAULT_ACCOUNT_ID", process.env.CLOUDFLARE_DEFAULT_ACCOUNT_ID!),
|
new sst.Secret("CLOUDFLARE_DEFAULT_ACCOUNT_ID", process.env.CLOUDFLARE_DEFAULT_ACCOUNT_ID!),
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { SECRET } from "./secret"
|
||||||
|
import { domain } from "./stage"
|
||||||
|
|
||||||
|
const storage = new sst.cloudflare.Bucket("EnterpriseStorage")
|
||||||
|
|
||||||
|
const enterprise = new sst.cloudflare.x.SolidStart("Enterprise", {
|
||||||
|
domain: "enterprise." + domain,
|
||||||
|
path: "packages/enterprise",
|
||||||
|
buildCommand: "bun run build:cloudflare",
|
||||||
|
environment: {
|
||||||
|
OPENCODE_STORAGE_ADAPTER: "r2",
|
||||||
|
OPENCODE_STORAGE_ACCOUNT_ID: sst.cloudflare.DEFAULT_ACCOUNT_ID,
|
||||||
|
OPENCODE_STORAGE_ACCESS_KEY_ID: SECRET.R2AccessKey.value,
|
||||||
|
OPENCODE_STORAGE_SECRET_ACCESS_KEY: SECRET.R2SecretKey.value,
|
||||||
|
OPENCODE_STORAGE_BUCKET: storage.name,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export const SECRET = {
|
||||||
|
R2AccessKey: new sst.Secret("R2AccessKey", "unknown"),
|
||||||
|
R2SecretKey: new sst.Secret("R2SecretKey", "unknown"),
|
||||||
|
}
|
||||||
@@ -2,9 +2,8 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
APP=opencode
|
APP=opencode
|
||||||
|
|
||||||
|
MUTED='\033[0;2m'
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
ORANGE='\033[38;2;255;140;0m'
|
ORANGE='\033[38;2;255;140;0m'
|
||||||
NC='\033[0m' # No Color
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
@@ -26,7 +25,11 @@ elif [[ "$arch" == "x86_64" ]]; then
|
|||||||
arch="x64"
|
arch="x64"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
filename="$APP-$os-$arch.zip"
|
if [ "$os" = "linux" ]; then
|
||||||
|
filename="$APP-$os-$arch.tar.gz"
|
||||||
|
else
|
||||||
|
filename="$APP-$os-$arch.zip"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
case "$filename" in
|
case "$filename" in
|
||||||
@@ -45,6 +48,18 @@ case "$filename" in
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
if [ "$os" = "linux" ]; then
|
||||||
|
if ! command -v tar >/dev/null 2>&1; then
|
||||||
|
echo -e "${RED}Error: 'tar' is required but not installed.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if ! command -v unzip >/dev/null 2>&1; then
|
||||||
|
echo -e "${RED}Error: 'unzip' is required but not installed.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
INSTALL_DIR=$HOME/.opencode/bin
|
INSTALL_DIR=$HOME/.opencode/bin
|
||||||
mkdir -p "$INSTALL_DIR"
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
|
||||||
@@ -67,8 +82,8 @@ print_message() {
|
|||||||
local color=""
|
local color=""
|
||||||
|
|
||||||
case $level in
|
case $level in
|
||||||
info) color="${GREEN}" ;;
|
info) color="${NC}" ;;
|
||||||
warning) color="${YELLOW}" ;;
|
warning) color="${NC}" ;;
|
||||||
error) color="${RED}" ;;
|
error) color="${RED}" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@@ -86,19 +101,119 @@ check_version() {
|
|||||||
installed_version=$(echo $installed_version | awk '{print $2}')
|
installed_version=$(echo $installed_version | awk '{print $2}')
|
||||||
|
|
||||||
if [[ "$installed_version" != "$specific_version" ]]; then
|
if [[ "$installed_version" != "$specific_version" ]]; then
|
||||||
print_message info "Installed version: ${YELLOW}$installed_version."
|
print_message info "${MUTED}Installed version: ${NC}$installed_version."
|
||||||
else
|
else
|
||||||
print_message info "Version ${YELLOW}$specific_version${GREEN} already installed"
|
print_message info "${MUTED}Version ${NC}$specific_version${MUTED} already installed"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unbuffered_sed() {
|
||||||
|
if echo | sed -u -e "" >/dev/null 2>&1; then
|
||||||
|
sed -nu "$@"
|
||||||
|
elif echo | sed -l -e "" >/dev/null 2>&1; then
|
||||||
|
sed -nl "$@"
|
||||||
|
else
|
||||||
|
local pad="$(printf "\n%512s" "")"
|
||||||
|
sed -ne "s/$/\\${pad}/" "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
print_progress() {
|
||||||
|
local bytes="$1"
|
||||||
|
local length="$2"
|
||||||
|
[ "$length" -gt 0 ] || return 0
|
||||||
|
|
||||||
|
local width=50
|
||||||
|
local percent=$(( bytes * 100 / length ))
|
||||||
|
[ "$percent" -gt 100 ] && percent=100
|
||||||
|
local on=$(( percent * width / 100 ))
|
||||||
|
local off=$(( width - on ))
|
||||||
|
|
||||||
|
local filled=$(printf "%*s" "$on" "")
|
||||||
|
filled=${filled// /■}
|
||||||
|
local empty=$(printf "%*s" "$off" "")
|
||||||
|
empty=${empty// /・}
|
||||||
|
|
||||||
|
printf "\r${ORANGE}%s%s %3d%%${NC}" "$filled" "$empty" "$percent" >&4
|
||||||
|
}
|
||||||
|
|
||||||
|
download_with_progress() {
|
||||||
|
local url="$1"
|
||||||
|
local output="$2"
|
||||||
|
|
||||||
|
if [ -t 2 ]; then
|
||||||
|
exec 4>&2
|
||||||
|
else
|
||||||
|
exec 4>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
local tmp_dir=${TMPDIR:-/tmp}
|
||||||
|
local basename="${tmp_dir}/opencode_install_$$"
|
||||||
|
local tracefile="${basename}.trace"
|
||||||
|
|
||||||
|
rm -f "$tracefile"
|
||||||
|
mkfifo "$tracefile"
|
||||||
|
|
||||||
|
# Hide cursor
|
||||||
|
printf "\033[?25l" >&4
|
||||||
|
|
||||||
|
trap "trap - RETURN; rm -f \"$tracefile\"; printf '\033[?25h' >&4; exec 4>&-" RETURN
|
||||||
|
|
||||||
|
(
|
||||||
|
curl --trace-ascii "$tracefile" -s -L -o "$output" "$url"
|
||||||
|
) &
|
||||||
|
local curl_pid=$!
|
||||||
|
|
||||||
|
unbuffered_sed \
|
||||||
|
-e 'y/ACDEGHLNORTV/acdeghlnortv/' \
|
||||||
|
-e '/^0000: content-length:/p' \
|
||||||
|
-e '/^<= recv data/p' \
|
||||||
|
"$tracefile" | \
|
||||||
|
{
|
||||||
|
local length=0
|
||||||
|
local bytes=0
|
||||||
|
|
||||||
|
while IFS=" " read -r -a line; do
|
||||||
|
[ "${#line[@]}" -lt 2 ] && continue
|
||||||
|
local tag="${line[0]} ${line[1]}"
|
||||||
|
|
||||||
|
if [ "$tag" = "0000: content-length:" ]; then
|
||||||
|
length="${line[2]}"
|
||||||
|
length=$(echo "$length" | tr -d '\r')
|
||||||
|
bytes=0
|
||||||
|
elif [ "$tag" = "<= recv" ]; then
|
||||||
|
local size="${line[3]}"
|
||||||
|
bytes=$(( bytes + size ))
|
||||||
|
if [ "$length" -gt 0 ]; then
|
||||||
|
print_progress "$bytes" "$length"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
wait $curl_pid
|
||||||
|
local ret=$?
|
||||||
|
echo "" >&4
|
||||||
|
return $ret
|
||||||
|
}
|
||||||
|
|
||||||
download_and_install() {
|
download_and_install() {
|
||||||
print_message info "Downloading ${ORANGE}opencode ${GREEN}version: ${YELLOW}$specific_version ${GREEN}..."
|
print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}version: ${NC}$specific_version"
|
||||||
mkdir -p opencodetmp && cd opencodetmp
|
mkdir -p opencodetmp && cd opencodetmp
|
||||||
curl -# -L -o "$filename" "$url"
|
|
||||||
unzip -q "$filename"
|
if [[ "$os" == "windows" ]] || ! download_with_progress "$url" "$filename"; then
|
||||||
|
# Fallback to standard curl on Windows or if custom progress fails
|
||||||
|
curl -# -L -o "$filename" "$url"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$os" = "linux" ]; then
|
||||||
|
tar -xzf "$filename"
|
||||||
|
else
|
||||||
|
unzip -q "$filename"
|
||||||
|
fi
|
||||||
|
|
||||||
mv opencode "$INSTALL_DIR"
|
mv opencode "$INSTALL_DIR"
|
||||||
chmod 755 "${INSTALL_DIR}/opencode"
|
chmod 755 "${INSTALL_DIR}/opencode"
|
||||||
cd .. && rm -rf opencodetmp
|
cd .. && rm -rf opencodetmp
|
||||||
@@ -117,7 +232,7 @@ add_to_path() {
|
|||||||
elif [[ -w $config_file ]]; then
|
elif [[ -w $config_file ]]; then
|
||||||
echo -e "\n# opencode" >> "$config_file"
|
echo -e "\n# opencode" >> "$config_file"
|
||||||
echo "$command" >> "$config_file"
|
echo "$command" >> "$config_file"
|
||||||
print_message info "Successfully added ${ORANGE}opencode ${GREEN}to \$PATH in $config_file"
|
print_message info "${MUTED}Successfully added ${NC}opencode ${MUTED}to \$PATH in ${NC}$config_file"
|
||||||
else
|
else
|
||||||
print_message warning "Manually add the directory to $config_file (or similar):"
|
print_message warning "Manually add the directory to $config_file (or similar):"
|
||||||
print_message info " $command"
|
print_message info " $command"
|
||||||
@@ -191,3 +306,20 @@ if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then
|
|||||||
echo "$INSTALL_DIR" >> $GITHUB_PATH
|
echo "$INSTALL_DIR" >> $GITHUB_PATH
|
||||||
print_message info "Added $INSTALL_DIR to \$GITHUB_PATH"
|
print_message info "Added $INSTALL_DIR to \$GITHUB_PATH"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo -e ""
|
||||||
|
echo -e "${MUTED} ${NC} ▄ "
|
||||||
|
echo -e "${MUTED}█▀▀█ █▀▀█ █▀▀█ █▀▀▄ ${NC}█▀▀▀ █▀▀█ █▀▀█ █▀▀█"
|
||||||
|
echo -e "${MUTED}█░░█ █░░█ █▀▀▀ █░░█ ${NC}█░░░ █░░█ █░░█ █▀▀▀"
|
||||||
|
echo -e "${MUTED}▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ${NC}▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀"
|
||||||
|
echo -e ""
|
||||||
|
echo -e ""
|
||||||
|
echo -e "${MUTED}To get started, navigate to a project and run:${NC}"
|
||||||
|
echo -e "opencode ${MUTED}Use free models${NC}"
|
||||||
|
echo -e "opencode auth login ${MUTED}Add paid provider API keys${NC}"
|
||||||
|
echo -e "opencode help ${MUTED}List commands and options${NC}"
|
||||||
|
echo -e ""
|
||||||
|
echo -e "${MUTED}For more information visit ${NC}https://opencode.ai/docs"
|
||||||
|
echo -e ""
|
||||||
|
echo -e ""
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import solidPlugin from "./node_modules/@opentui/solid/scripts/solid-plugin"
|
||||||
|
import path from "path"
|
||||||
|
import fs from "fs"
|
||||||
|
|
||||||
|
const dir = process.cwd()
|
||||||
|
const parser = fs.realpathSync(path.join(dir, "node_modules/@opentui/core/parser.worker.js"))
|
||||||
|
const worker = "./src/cli/cmd/tui/worker.ts"
|
||||||
|
const version = process.env.OPENCODE_VERSION ?? "local"
|
||||||
|
const channel = process.env.OPENCODE_CHANNEL ?? "local"
|
||||||
|
|
||||||
|
fs.rmSync(path.join(dir, "dist"), { recursive: true, force: true })
|
||||||
|
|
||||||
|
const result = await Bun.build({
|
||||||
|
entrypoints: ["./src/index.ts", worker, parser],
|
||||||
|
outdir: "./dist",
|
||||||
|
target: "bun",
|
||||||
|
sourcemap: "none",
|
||||||
|
tsconfig: "./tsconfig.json",
|
||||||
|
plugins: [solidPlugin],
|
||||||
|
external: ["@opentui/core"],
|
||||||
|
define: {
|
||||||
|
OPENCODE_VERSION: `'${version}'`,
|
||||||
|
OPENCODE_CHANNEL: `'${channel}'`,
|
||||||
|
// Leave undefined so runtime picks bundled/dist worker or fallback in code.
|
||||||
|
OPENCODE_WORKER_PATH: "undefined",
|
||||||
|
OTUI_TREE_SITTER_WORKER_PATH: 'new URL("./cli/cmd/tui/parser.worker.js", import.meta.url).href',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.error("bundle failed")
|
||||||
|
for (const log of result.logs) console.error(log)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const parserOut = path.join(dir, "dist/src/cli/cmd/tui/parser.worker.js")
|
||||||
|
fs.mkdirSync(path.dirname(parserOut), { recursive: true })
|
||||||
|
await Bun.write(parserOut, Bun.file(parser))
|
||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": "sha256-bPiUpHGtgwVxHQHXBprpc6fFeJqW6/x7dwtQZBq29oU="
|
"nodeModules": "sha256-m7hL9Uzqk+oa2/FtgkzEPgi+m/VZP1SvjpgYHNjiS1c="
|
||||||
}
|
}
|
||||||
|
|||||||
+60
-35
@@ -1,4 +1,4 @@
|
|||||||
{ lib, stdenv, stdenvNoCC, bun, fzf, ripgrep, makeBinaryWrapper }:
|
{ lib, stdenvNoCC, bun, fzf, ripgrep, makeBinaryWrapper }:
|
||||||
args:
|
args:
|
||||||
let
|
let
|
||||||
scripts = args.scripts;
|
scripts = args.scripts;
|
||||||
@@ -28,64 +28,89 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
|||||||
makeBinaryWrapper
|
makeBinaryWrapper
|
||||||
];
|
];
|
||||||
|
|
||||||
configurePhase = ''
|
|
||||||
runHook preConfigure
|
|
||||||
cp -R ${finalAttrs.node_modules}/. .
|
|
||||||
runHook postConfigure
|
|
||||||
'';
|
|
||||||
|
|
||||||
env.MODELS_DEV_API_JSON = args.modelsDev;
|
env.MODELS_DEV_API_JSON = args.modelsDev;
|
||||||
env.OPENCODE_VERSION = args.version;
|
env.OPENCODE_VERSION = args.version;
|
||||||
env.OPENCODE_CHANNEL = "stable";
|
env.OPENCODE_CHANNEL = "stable";
|
||||||
|
dontConfigure = true;
|
||||||
|
|
||||||
buildPhase = ''
|
buildPhase = ''
|
||||||
runHook preBuild
|
runHook preBuild
|
||||||
|
|
||||||
cp ${scripts + "/bun-build.ts"} bun-build.ts
|
cp -r ${finalAttrs.node_modules}/node_modules .
|
||||||
|
cp -r ${finalAttrs.node_modules}/packages .
|
||||||
|
|
||||||
substituteInPlace bun-build.ts \
|
(
|
||||||
--replace '@VERSION@' "${finalAttrs.version}"
|
cd packages/opencode
|
||||||
|
|
||||||
export BUN_COMPILE_TARGET=${args.target}
|
chmod -R u+w ./node_modules
|
||||||
bun --bun bun-build.ts
|
mkdir -p ./node_modules/@opencode-ai
|
||||||
|
rm -f ./node_modules/@opencode-ai/{script,sdk,plugin}
|
||||||
|
ln -s $(pwd)/../../packages/script ./node_modules/@opencode-ai/script
|
||||||
|
ln -s $(pwd)/../../packages/sdk/js ./node_modules/@opencode-ai/sdk
|
||||||
|
ln -s $(pwd)/../../packages/plugin ./node_modules/@opencode-ai/plugin
|
||||||
|
|
||||||
|
cp ${./bundle.ts} ./bundle.ts
|
||||||
|
chmod +x ./bundle.ts
|
||||||
|
bun run ./bundle.ts
|
||||||
|
)
|
||||||
|
|
||||||
runHook postBuild
|
runHook postBuild
|
||||||
'';
|
'';
|
||||||
|
|
||||||
dontStrip = true;
|
|
||||||
|
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
runHook preInstall
|
runHook preInstall
|
||||||
|
|
||||||
cd packages/opencode
|
cd packages/opencode
|
||||||
if [ ! -f opencode ]; then
|
if [ ! -d dist ]; then
|
||||||
echo "ERROR: opencode binary not found in $(pwd)"
|
echo "ERROR: dist directory missing after bundle step"
|
||||||
ls -la
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [ ! -f opencode-worker.js ]; then
|
|
||||||
echo "ERROR: opencode worker bundle not found in $(pwd)"
|
|
||||||
ls -la
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
install -Dm755 opencode $out/bin/opencode
|
mkdir -p $out/lib/opencode
|
||||||
install -Dm644 opencode-worker.js $out/bin/opencode-worker.js
|
cp -r dist $out/lib/opencode/
|
||||||
if [ -f opencode-assets.manifest ]; then
|
chmod -R u+w $out/lib/opencode/dist
|
||||||
while IFS= read -r asset; do
|
|
||||||
[ -z "$asset" ] && continue
|
# Select bundled worker assets deterministically (sorted find output)
|
||||||
if [ ! -f "$asset" ]; then
|
worker_file=$(find "$out/lib/opencode/dist" -type f \( -path '*/tui/worker.*' -o -name 'worker.*' \) | sort | head -n1)
|
||||||
echo "ERROR: referenced asset \"$asset\" missing"
|
parser_worker_file=$(find "$out/lib/opencode/dist" -type f -name 'parser.worker.*' | sort | head -n1)
|
||||||
exit 1
|
if [ -z "$worker_file" ]; then
|
||||||
fi
|
echo "ERROR: bundled worker not found"
|
||||||
install -Dm644 "$asset" "$out/bin/$(basename "$asset")"
|
exit 1
|
||||||
done < opencode-assets.manifest
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
main_wasm=$(printf '%s\n' "$out"/lib/opencode/dist/tree-sitter-*.wasm | sort | head -n1)
|
||||||
|
wasm_list=$(find "$out/lib/opencode/dist" -maxdepth 1 -name 'tree-sitter-*.wasm' -print)
|
||||||
|
for patch_file in "$worker_file" "$parser_worker_file"; do
|
||||||
|
[ -z "$patch_file" ] && continue
|
||||||
|
[ ! -f "$patch_file" ] && continue
|
||||||
|
if [ -n "$wasm_list" ] && grep -q 'tree-sitter' "$patch_file"; then
|
||||||
|
# Rewrite wasm references to absolute store paths to avoid runtime resolve failures.
|
||||||
|
bun --bun ${scripts + "/patch-wasm.ts"} "$patch_file" "$main_wasm" $wasm_list
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
mkdir -p $out/lib/opencode/node_modules
|
||||||
|
cp -r ../../node_modules/.bun $out/lib/opencode/node_modules/
|
||||||
|
mkdir -p $out/lib/opencode/node_modules/@opentui
|
||||||
|
|
||||||
|
mkdir -p $out/bin
|
||||||
|
makeWrapper ${bun}/bin/bun $out/bin/opencode \
|
||||||
|
--add-flags "run" \
|
||||||
|
--add-flags "$out/lib/opencode/dist/src/index.js" \
|
||||||
|
--prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]} \
|
||||||
|
--argv0 opencode
|
||||||
|
|
||||||
runHook postInstall
|
runHook postInstall
|
||||||
'';
|
'';
|
||||||
|
|
||||||
postFixup = ''
|
postInstall = ''
|
||||||
wrapProgram "$out/bin/opencode" --prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]}
|
for pkg in $out/lib/opencode/node_modules/.bun/@opentui+core-* $out/lib/opencode/node_modules/.bun/@opentui+solid-* $out/lib/opencode/node_modules/.bun/@opentui+core@* $out/lib/opencode/node_modules/.bun/@opentui+solid@*; do
|
||||||
|
if [ -d "$pkg" ]; then
|
||||||
|
pkgName=$(basename "$pkg" | sed 's/@opentui+\([^@]*\)@.*/\1/')
|
||||||
|
ln -sf ../.bun/$(basename "$pkg")/node_modules/@opentui/$pkgName \
|
||||||
|
$out/lib/opencode/node_modules/@opentui/$pkgName
|
||||||
|
fi
|
||||||
|
done
|
||||||
'';
|
'';
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
|
|||||||
@@ -24,15 +24,13 @@ for (const entry of directories) {
|
|||||||
if (!info.isDirectory()) {
|
if (!info.isDirectory()) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const marker = entry.lastIndexOf("@")
|
const parsed = parseEntry(entry)
|
||||||
if (marker <= 0) {
|
if (!parsed) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const slug = entry.slice(0, marker).replace(/\+/g, "/")
|
const list = versions.get(parsed.name) ?? []
|
||||||
const version = entry.slice(marker + 1)
|
list.push({ dir: full, version: parsed.version, label: entry })
|
||||||
const list = versions.get(slug) ?? []
|
versions.set(parsed.name, list)
|
||||||
list.push({ dir: full, version, label: entry })
|
|
||||||
versions.set(slug, list)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const semverModule = (await import(join(bunRoot, "node_modules/semver"))) as
|
const semverModule = (await import(join(bunRoot, "node_modules/semver"))) as
|
||||||
@@ -79,6 +77,12 @@ for (const [slug, entry] of Array.from(selections.entries()).sort((a, b) => a[0]
|
|||||||
await mkdir(parent, { recursive: true })
|
await mkdir(parent, { recursive: true })
|
||||||
const linkPath = join(parent, leaf)
|
const linkPath = join(parent, leaf)
|
||||||
const desired = join(entry.dir, "node_modules", slug)
|
const desired = join(entry.dir, "node_modules", slug)
|
||||||
|
const exists = await lstat(desired)
|
||||||
|
.then((info) => info.isDirectory())
|
||||||
|
.catch(() => false)
|
||||||
|
if (!exists) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
const relativeTarget = relative(parent, desired)
|
const relativeTarget = relative(parent, desired)
|
||||||
const resolved = relativeTarget.length === 0 ? "." : relativeTarget
|
const resolved = relativeTarget.length === 0 ? "." : relativeTarget
|
||||||
await rm(linkPath, { recursive: true, force: true })
|
await rm(linkPath, { recursive: true, force: true })
|
||||||
@@ -94,3 +98,16 @@ for (const line of rewrites.slice(0, 20)) {
|
|||||||
if (rewrites.length > 20) {
|
if (rewrites.length > 20) {
|
||||||
console.log(" ...")
|
console.log(" ...")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseEntry(label: string) {
|
||||||
|
const marker = label.startsWith("@") ? label.indexOf("@", 1) : label.indexOf("@")
|
||||||
|
if (marker <= 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const name = label.slice(0, marker).replace(/\+/g, "/")
|
||||||
|
const version = label.slice(marker + 1)
|
||||||
|
if (!name || !version) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return { name, version }
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rewrite tree-sitter wasm references inside a JS file to absolute paths.
|
||||||
|
* argv: [node, script, file, mainWasm, ...wasmPaths]
|
||||||
|
*/
|
||||||
|
const [, , file, mainWasm, ...wasmPaths] = process.argv
|
||||||
|
|
||||||
|
if (!file || !mainWasm) {
|
||||||
|
console.error("usage: patch-wasm <file> <mainWasm> [wasmPaths...]")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fs.readFileSync(file, "utf8")
|
||||||
|
const byName = new Map<string, string>()
|
||||||
|
|
||||||
|
for (const wasm of wasmPaths) {
|
||||||
|
const name = path.basename(wasm)
|
||||||
|
byName.set(name, wasm)
|
||||||
|
}
|
||||||
|
|
||||||
|
let next = content
|
||||||
|
|
||||||
|
for (const [name, wasmPath] of byName) {
|
||||||
|
next = next.replaceAll(name, wasmPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
next = next.replaceAll("tree-sitter.wasm", mainWasm).replaceAll("web-tree-sitter/tree-sitter.wasm", mainWasm)
|
||||||
|
|
||||||
|
// Collapse any relative prefixes before absolute store paths (e.g., "../../../..//nix/store/...")
|
||||||
|
next = next.replace(/(\.\/)+/g, "./")
|
||||||
|
next = next.replace(/(\.\.\/)+\/?(\/nix\/store[^"']+)/g, "/$2")
|
||||||
|
next = next.replace(/(["'])\/{2,}(\/nix\/store[^"']+)(["'])/g, "$1/$2$3")
|
||||||
|
next = next.replace(/(["'])\/\/(nix\/store[^"']+)(["'])/g, "$1/$2$3")
|
||||||
|
|
||||||
|
if (next !== content) fs.writeFileSync(file, next)
|
||||||
+13
-10
@@ -4,12 +4,13 @@
|
|||||||
"description": "AI-powered development tool",
|
"description": "AI-powered development tool",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "bun@1.3.2",
|
"packageManager": "bun@1.3.3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||||
"typecheck": "bun turbo typecheck",
|
"typecheck": "bun turbo typecheck",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"random": "echo 'Random script'"
|
"random": "echo 'Random script'",
|
||||||
|
"hello": "echo 'Hello World!'"
|
||||||
},
|
},
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"packages": [
|
"packages": [
|
||||||
@@ -19,33 +20,37 @@
|
|||||||
"packages/slack"
|
"packages/slack"
|
||||||
],
|
],
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@types/bun": "1.3.0",
|
"@types/bun": "1.3.3",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"ulid": "3.0.1",
|
"ulid": "3.0.1",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
|
"@types/luxon": "3.7.1",
|
||||||
"@types/node": "22.13.9",
|
"@types/node": "22.13.9",
|
||||||
"@tsconfig/node22": "22.0.2",
|
"@tsconfig/node22": "22.0.2",
|
||||||
"@tsconfig/bun": "1.0.9",
|
"@tsconfig/bun": "1.0.9",
|
||||||
"@cloudflare/workers-types": "4.20251008.0",
|
"@cloudflare/workers-types": "4.20251008.0",
|
||||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||||
"@pierre/precision-diffs": "0.4.4",
|
"@pierre/precision-diffs": "0.5.4",
|
||||||
"@solidjs/meta": "0.29.4",
|
|
||||||
"@tailwindcss/vite": "4.1.11",
|
"@tailwindcss/vite": "4.1.11",
|
||||||
"diff": "8.0.2",
|
"diff": "8.0.2",
|
||||||
"ai": "5.0.97",
|
"ai": "5.0.97",
|
||||||
"hono": "4.7.10",
|
"hono": "4.7.10",
|
||||||
|
"hono-openapi": "1.1.1",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"luxon": "3.6.1",
|
"luxon": "3.6.1",
|
||||||
"typescript": "5.8.2",
|
"typescript": "5.8.2",
|
||||||
"@typescript/native-preview": "7.0.0-dev.20251014.1",
|
"@typescript/native-preview": "7.0.0-dev.20251014.1",
|
||||||
"zod": "4.1.8",
|
"zod": "4.1.8",
|
||||||
"remeda": "2.26.0",
|
"remeda": "2.26.0",
|
||||||
"solid-js": "1.9.9",
|
|
||||||
"solid-list": "0.3.0",
|
"solid-list": "0.3.0",
|
||||||
"tailwindcss": "4.1.11",
|
"tailwindcss": "4.1.11",
|
||||||
"virtua": "0.42.3",
|
"virtua": "0.42.3",
|
||||||
"vite": "7.1.4",
|
"vite": "7.1.4",
|
||||||
"vite-plugin-solid": "2.11.8"
|
"@solidjs/meta": "0.29.4",
|
||||||
|
"@solidjs/router": "0.15.4",
|
||||||
|
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
|
||||||
|
"solid-js": "1.9.10",
|
||||||
|
"vite-plugin-solid": "2.11.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -56,6 +61,7 @@
|
|||||||
"turbo": "2.5.6"
|
"turbo": "2.5.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "3.933.0",
|
||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*"
|
"@opencode-ai/sdk": "workspace:*"
|
||||||
},
|
},
|
||||||
@@ -76,9 +82,6 @@
|
|||||||
"tree-sitter-bash",
|
"tree-sitter-bash",
|
||||||
"web-tree-sitter"
|
"web-tree-sitter"
|
||||||
],
|
],
|
||||||
"patchedDependencies": {
|
|
||||||
"@solidjs/start@1.1.7": "patches/@solidjs%2Fstart@1.1.7.patch"
|
|
||||||
},
|
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"@types/node": "catalog:"
|
"@types/node": "catalog:"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ dist
|
|||||||
.output
|
.output
|
||||||
.vercel
|
.vercel
|
||||||
.netlify
|
.netlify
|
||||||
.vinxi
|
|
||||||
app.config.timestamp_*.js
|
app.config.timestamp_*.js
|
||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { defineConfig } from "@solidjs/start/config"
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
middleware: "./src/middleware.ts",
|
|
||||||
vite: {
|
|
||||||
server: {
|
|
||||||
allowedHosts: true,
|
|
||||||
},
|
|
||||||
build: {
|
|
||||||
rollupOptions: {
|
|
||||||
external: ["cloudflare:workers"],
|
|
||||||
},
|
|
||||||
minify: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
compatibilityDate: "2024-09-19",
|
|
||||||
preset: "cloudflare_module",
|
|
||||||
cloudflare: {
|
|
||||||
nodeCompat: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
|
"version": "1.0.109",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
"dev": "vinxi dev --host 0.0.0.0",
|
"dev": "vite dev --host 0.0.0.0",
|
||||||
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
|
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
|
||||||
"build": "./script/generate-sitemap.ts && vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
|
"build": "./script/generate-sitemap.ts && vite build && ../../opencode/script/schema.ts ./.output/public/config.json",
|
||||||
"start": "vinxi start",
|
"start": "vite start"
|
||||||
"version": "1.0.85"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
@@ -17,17 +18,19 @@
|
|||||||
"@opencode-ai/console-core": "workspace:*",
|
"@opencode-ai/console-core": "workspace:*",
|
||||||
"@opencode-ai/console-mail": "workspace:*",
|
"@opencode-ai/console-mail": "workspace:*",
|
||||||
"@opencode-ai/console-resource": "workspace:*",
|
"@opencode-ai/console-resource": "workspace:*",
|
||||||
"@solidjs/meta": "^0.29.4",
|
"@solidjs/meta": "catalog:",
|
||||||
"@solidjs/router": "^0.15.0",
|
"@solidjs/router": "catalog:",
|
||||||
"@solidjs/start": "^1.1.0",
|
"@solidjs/start": "catalog:",
|
||||||
"chart.js": "4.5.1",
|
"chart.js": "4.5.1",
|
||||||
|
"nitro": "3.0.1-alpha.1",
|
||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
"vinxi": "^0.5.7",
|
"vite": "catalog:",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@typescript/native-preview": "catalog:",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:"
|
"wrangler": "4.50.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22"
|
"node": ">=22"
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="400" height="400" fill="#0E0E0E"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M312 340H88V60H312V340ZM256 116H144V284H256V116Z" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 269 B After Width: | Height: | Size: 42 B |
+1
@@ -0,0 +1 @@
|
|||||||
|
../../../ui/src/assets/favicon/favicon.svg
|
||||||
|
Before Width: | Height: | Size: 269 B After Width: | Height: | Size: 42 B |
@@ -202,7 +202,7 @@ export function IconZai(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function IconGoogle(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
export function IconGemini(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||||
return (
|
return (
|
||||||
<svg {...props} viewBox="0 0 50 50" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
<svg {...props} viewBox="0 0 50 50" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M49.04,24.001l-1.082-0.043h-0.001C36.134,23.492,26.508,13.866,26.042,2.043L25.999,0.96C25.978,0.424,25.537,0,25,0 s-0.978,0.424-0.999,0.96l-0.043,1.083C23.492,13.866,13.866,23.492,2.042,23.958L0.96,24.001C0.424,24.022,0,24.463,0,25 c0,0.537,0.424,0.978,0.961,0.999l1.082,0.042c11.823,0.467,21.449,10.093,21.915,21.916l0.043,1.083C24.022,49.576,24.463,50,25,50 s0.978-0.424,0.999-0.96l0.043-1.083c0.466-11.823,10.092-21.449,21.915-21.916l1.082-0.042C49.576,25.978,50,25.537,50,25 C50,24.463,49.576,24.022,49.04,24.001z"></path>
|
<path d="M49.04,24.001l-1.082-0.043h-0.001C36.134,23.492,26.508,13.866,26.042,2.043L25.999,0.96C25.978,0.424,25.537,0,25,0 s-0.978,0.424-0.999,0.96l-0.043,1.083C23.492,13.866,13.866,23.492,2.042,23.958L0.96,24.001C0.424,24.022,0,24.463,0,25 c0,0.537,0.424,0.978,0.961,0.999l1.082,0.042c11.823,0.467,21.449,10.093,21.915,21.916l0.043,1.083C24.022,49.576,24.463,50,25,50 s0.978-0.424,0.999-0.96l0.043-1.083c0.466-11.823,10.092-21.449,21.915-21.916l1.082-0.042C49.576,25.978,50,25.537,50,25 C50,24.463,49.576,24.022,49.04,24.001z"></path>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useSession } from "vinxi/http"
|
import { useSession } from "@solidjs/start/http"
|
||||||
|
|
||||||
export interface AuthSession {
|
export interface AuthSession {
|
||||||
account?: Record<
|
account?: Record<
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default createHandler(
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.svg" />
|
<link rel="shortcut icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta property="og:image" content="/social-share.png" />
|
<meta property="og:image" content="/social-share.png" />
|
||||||
<meta property="twitter:image" content="/social-share.png" />
|
<meta property="twitter:image" content="/social-share.png" />
|
||||||
{assets}
|
{assets}
|
||||||
|
|||||||
Vendored
+4
@@ -1 +1,5 @@
|
|||||||
/// <reference types="@solidjs/start/env" />
|
/// <reference types="@solidjs/start/env" />
|
||||||
|
|
||||||
|
export declare module "@solidjs/start/server" {
|
||||||
|
export type APIEvent = { request: Request }
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { defineMiddleware } from "vinxi/http"
|
import { createMiddleware } from "@solidjs/start/middleware"
|
||||||
|
|
||||||
export default defineMiddleware({
|
export default createMiddleware({
|
||||||
onBeforeResponse() {},
|
onBeforeResponse() {},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export async function GET(input: APIEvent) {
|
|||||||
return {
|
return {
|
||||||
...value,
|
...value,
|
||||||
account: {
|
account: {
|
||||||
|
...value.account,
|
||||||
[id]: {
|
[id]: {
|
||||||
id,
|
id,
|
||||||
email: decoded.subject.properties.email,
|
email: decoded.subject.properties.email,
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { redirect } from "@solidjs/router"
|
||||||
|
import { APIEvent } from "@solidjs/start"
|
||||||
|
import { useAuthSession } from "~/context/auth.session"
|
||||||
|
|
||||||
|
export async function GET(event: APIEvent) {
|
||||||
|
const auth = await useAuthSession()
|
||||||
|
const current = auth.data.current
|
||||||
|
if (current)
|
||||||
|
await auth.update((val) => {
|
||||||
|
delete val.account?.[current]
|
||||||
|
const first = Object.keys(val.account ?? {})[0]
|
||||||
|
val.current = first
|
||||||
|
event!.locals.actor = undefined
|
||||||
|
return val
|
||||||
|
})
|
||||||
|
return redirect("/zen")
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { APIEvent } from "@solidjs/start"
|
||||||
|
import { useAuthSession } from "~/context/auth.session"
|
||||||
|
|
||||||
|
export async function GET(input: APIEvent) {
|
||||||
|
const session = await useAuthSession()
|
||||||
|
return Response.json(session.data)
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import "./index.css"
|
import "./index.css"
|
||||||
import { Title, Meta, Link } from "@solidjs/meta"
|
import { Title, Meta, Link } from "@solidjs/meta"
|
||||||
import { HttpHeader } from "@solidjs/start"
|
// import { HttpHeader } from "@solidjs/start"
|
||||||
import video from "../asset/lander/opencode-min.mp4"
|
import video from "../asset/lander/opencode-min.mp4"
|
||||||
import videoPoster from "../asset/lander/opencode-poster.png"
|
import videoPoster from "../asset/lander/opencode-poster.png"
|
||||||
import { IconCopy, IconCheck } from "../component/icon"
|
import { IconCopy, IconCheck } from "../component/icon"
|
||||||
@@ -42,10 +42,9 @@ export default function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main data-page="opencode">
|
<main data-page="opencode">
|
||||||
<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />
|
{/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/}
|
||||||
<Title>OpenCode | The AI coding agent built for the terminal</Title>
|
<Title>OpenCode | The AI coding agent built for the terminal</Title>
|
||||||
<Link rel="canonical" href={config.baseUrl} />
|
<Link rel="canonical" href={config.baseUrl} />
|
||||||
<Link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
||||||
<Meta property="og:image" content="/social-share.png" />
|
<Meta property="og:image" content="/social-share.png" />
|
||||||
<Meta name="twitter:image" content="/social-share.png" />
|
<Meta name="twitter:image" content="/social-share.png" />
|
||||||
<div data-component="container">
|
<div data-component="container">
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
[data-slot="item"] {
|
[data-slot="item"] {
|
||||||
color: var(--color-danger);
|
color: var(--color-danger);
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { action, redirect } from "@solidjs/router"
|
import { action } from "@solidjs/router"
|
||||||
import { getRequestEvent } from "solid-js/web"
|
import { getRequestEvent } from "solid-js/web"
|
||||||
import { useAuthSession } from "~/context/auth.session"
|
import { useAuthSession } from "~/context/auth.session"
|
||||||
import { Dropdown } from "~/component/dropdown"
|
import { Dropdown } from "~/component/dropdown"
|
||||||
@@ -17,18 +17,15 @@ const logout = action(async () => {
|
|||||||
event!.locals.actor = undefined
|
event!.locals.actor = undefined
|
||||||
return val
|
return val
|
||||||
})
|
})
|
||||||
throw redirect("/zen")
|
}, "auth.logout")
|
||||||
})
|
|
||||||
|
|
||||||
export function UserMenu(props: { email: string | null | undefined }) {
|
export function UserMenu(props: { email: string | null | undefined }) {
|
||||||
return (
|
return (
|
||||||
<div data-component="user-menu">
|
<div data-component="user-menu">
|
||||||
<Dropdown trigger={props.email ?? ""} align="right">
|
<Dropdown trigger={props.email ?? ""} align="right">
|
||||||
<form action={logout} method="post">
|
<a href="/auth/logout" data-slot="item">
|
||||||
<button type="submit" formaction={logout} data-slot="item">
|
Logout
|
||||||
Logout
|
</a>
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
|
|||||||
const userEmail = createAsync(() => getUserEmail(params.id!))
|
const userEmail = createAsync(() => getUserEmail(params.id!))
|
||||||
return (
|
return (
|
||||||
<main data-page="workspace">
|
<main data-page="workspace">
|
||||||
<Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" />
|
<Link rel="shortcut icon" type="image/svg+xml" href="/favicon-zen.svg" />
|
||||||
<header data-component="workspace-header">
|
<header data-component="workspace-header">
|
||||||
<div data-slot="header-brand">
|
<div data-slot="header-brand">
|
||||||
<A href="/" data-component="site-title">
|
<A href="/" data-component="site-title">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { querySessionInfo } from "../common"
|
|||||||
import {
|
import {
|
||||||
IconAlibaba,
|
IconAlibaba,
|
||||||
IconAnthropic,
|
IconAnthropic,
|
||||||
IconGoogle,
|
IconGemini,
|
||||||
IconMoonshotAI,
|
IconMoonshotAI,
|
||||||
IconOpenAI,
|
IconOpenAI,
|
||||||
IconStealth,
|
IconStealth,
|
||||||
@@ -117,7 +117,7 @@ export function ModelSection() {
|
|||||||
case "Anthropic":
|
case "Anthropic":
|
||||||
return <IconAnthropic width={16} height={16} />
|
return <IconAnthropic width={16} height={16} />
|
||||||
case "Google":
|
case "Google":
|
||||||
return <IconGoogle width={16} height={16} />
|
return <IconGemini width={16} height={16} />
|
||||||
case "Moonshot AI":
|
case "Moonshot AI":
|
||||||
return <IconMoonshotAI width={16} height={16} />
|
return <IconMoonshotAI width={16} height={16} />
|
||||||
case "Z.ai":
|
case "Z.ai":
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import styles from "./provider-section.module.css"
|
|||||||
const PROVIDERS = [
|
const PROVIDERS = [
|
||||||
{ name: "OpenAI", key: "openai", prefix: "sk-" },
|
{ name: "OpenAI", key: "openai", prefix: "sk-" },
|
||||||
{ name: "Anthropic", key: "anthropic", prefix: "sk-ant-" },
|
{ name: "Anthropic", key: "anthropic", prefix: "sk-ant-" },
|
||||||
|
{ name: "Google Gemini", key: "google", prefix: "AI" },
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
type Provider = (typeof PROVIDERS)[number]
|
type Provider = (typeof PROVIDERS)[number]
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ export function UsageSection() {
|
|||||||
return u.inputTokens + (u.cacheReadTokens ?? 0) + (u.cacheWrite5mTokens ?? 0) + (u.cacheWrite1hTokens ?? 0)
|
return u.inputTokens + (u.cacheReadTokens ?? 0) + (u.cacheWrite5mTokens ?? 0) + (u.cacheWrite1hTokens ?? 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const calculateTotalOutputTokens = (u: Awaited<ReturnType<typeof getUsageInfo>>[0]) => {
|
||||||
|
return u.outputTokens + (u.reasoningTokens ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
const goPrev = async () => {
|
const goPrev = async () => {
|
||||||
const usage = await getUsageInfo(params.id!, store.page - 1)
|
const usage = await getUsageInfo(params.id!, store.page - 1)
|
||||||
setStore({
|
setStore({
|
||||||
@@ -95,8 +99,11 @@ export function UsageSection() {
|
|||||||
{(usage, index) => {
|
{(usage, index) => {
|
||||||
const date = createMemo(() => new Date(usage.timeCreated))
|
const date = createMemo(() => new Date(usage.timeCreated))
|
||||||
const totalInputTokens = createMemo(() => calculateTotalInputTokens(usage))
|
const totalInputTokens = createMemo(() => calculateTotalInputTokens(usage))
|
||||||
const breakdownId = `breakdown-${index()}`
|
const totalOutputTokens = createMemo(() => calculateTotalOutputTokens(usage))
|
||||||
const isOpen = createMemo(() => openBreakdownId() === breakdownId)
|
const inputBreakdownId = `input-breakdown-${index()}`
|
||||||
|
const outputBreakdownId = `output-breakdown-${index()}`
|
||||||
|
const isInputOpen = createMemo(() => openBreakdownId() === inputBreakdownId)
|
||||||
|
const isOutputOpen = createMemo(() => openBreakdownId() === outputBreakdownId)
|
||||||
const isClaude = usage.model.toLowerCase().includes("claude")
|
const isClaude = usage.model.toLowerCase().includes("claude")
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
@@ -110,13 +117,13 @@ export function UsageSection() {
|
|||||||
data-slot="breakdown-button"
|
data-slot="breakdown-button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setOpenBreakdownId(isOpen() ? null : breakdownId)
|
setOpenBreakdownId(isInputOpen() ? null : inputBreakdownId)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconBreakdown />
|
<IconBreakdown />
|
||||||
</button>
|
</button>
|
||||||
<span onClick={() => setOpenBreakdownId(null)}>{totalInputTokens()}</span>
|
<span onClick={() => setOpenBreakdownId(null)}>{totalInputTokens()}</span>
|
||||||
<Show when={isOpen()}>
|
<Show when={isInputOpen()}>
|
||||||
<div data-slot="breakdown-popup" onClick={(e) => e.stopPropagation()}>
|
<div data-slot="breakdown-popup" onClick={(e) => e.stopPropagation()}>
|
||||||
<div data-slot="breakdown-row">
|
<div data-slot="breakdown-row">
|
||||||
<span data-slot="breakdown-label">Input</span>
|
<span data-slot="breakdown-label">Input</span>
|
||||||
@@ -136,7 +143,32 @@ export function UsageSection() {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td data-slot="usage-tokens">{usage.outputTokens}</td>
|
<td data-slot="usage-tokens">
|
||||||
|
<div data-slot="tokens-with-breakdown" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<button
|
||||||
|
data-slot="breakdown-button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setOpenBreakdownId(isOutputOpen() ? null : outputBreakdownId)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconBreakdown />
|
||||||
|
</button>
|
||||||
|
<span onClick={() => setOpenBreakdownId(null)}>{totalOutputTokens()}</span>
|
||||||
|
<Show when={isOutputOpen()}>
|
||||||
|
<div data-slot="breakdown-popup" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div data-slot="breakdown-row">
|
||||||
|
<span data-slot="breakdown-label">Output</span>
|
||||||
|
<span data-slot="breakdown-value">{usage.outputTokens}</span>
|
||||||
|
</div>
|
||||||
|
<div data-slot="breakdown-row">
|
||||||
|
<span data-slot="breakdown-label">Reasoning</span>
|
||||||
|
<span data-slot="breakdown-value">{usage.reasoningTokens ?? 0}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</td>
|
<td data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import "./index.css"
|
import "./index.css"
|
||||||
import { createAsync, query, redirect } from "@solidjs/router"
|
import { createAsync, query, redirect } from "@solidjs/router"
|
||||||
import { Title, Meta, Link } from "@solidjs/meta"
|
import { Title, Meta, Link } from "@solidjs/meta"
|
||||||
import { HttpHeader } from "@solidjs/start"
|
// import { HttpHeader } from "@solidjs/start"
|
||||||
import zenLogoLight from "../../asset/zen-ornate-light.svg"
|
import zenLogoLight from "../../asset/zen-ornate-light.svg"
|
||||||
import { config } from "~/config"
|
import { config } from "~/config"
|
||||||
import zenLogoDark from "../../asset/zen-ornate-dark.svg"
|
import zenLogoDark from "../../asset/zen-ornate-dark.svg"
|
||||||
@@ -18,23 +18,25 @@ import { Legal } from "~/component/legal"
|
|||||||
import { Footer } from "~/component/footer"
|
import { Footer } from "~/component/footer"
|
||||||
import { Header } from "~/component/header"
|
import { Header } from "~/component/header"
|
||||||
import { getLastSeenWorkspaceID } from "../workspace/common"
|
import { getLastSeenWorkspaceID } from "../workspace/common"
|
||||||
|
import { IconGemini } from "~/component/icon"
|
||||||
|
|
||||||
const checkLoggedIn = query(async () => {
|
const checkLoggedIn = query(async () => {
|
||||||
"use server"
|
"use server"
|
||||||
const workspaceID = await getLastSeenWorkspaceID()
|
const workspaceID = await getLastSeenWorkspaceID().catch(() => {})
|
||||||
if (workspaceID) throw redirect(`/workspace/${workspaceID}`)
|
if (workspaceID) throw redirect(`/workspace/${workspaceID}`)
|
||||||
}, "checkLoggedIn.get")
|
}, "checkLoggedIn.get")
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
createAsync(() => checkLoggedIn())
|
const loggedin = createAsync(() => checkLoggedIn())
|
||||||
return (
|
return (
|
||||||
<main data-page="zen">
|
<main data-page="zen">
|
||||||
<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />
|
{/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/}
|
||||||
<Title>OpenCode Zen | A curated set of reliable optimized models for coding agents</Title>
|
<Title>OpenCode Zen | A curated set of reliable optimized models for coding agents</Title>
|
||||||
<Link rel="canonical" href={`${config.baseUrl}/zen`} />
|
<Link rel="canonical" href={`${config.baseUrl}/zen`} />
|
||||||
<Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" />
|
<Link rel="shortcut icon" type="image/svg+xml" href="/favicon-zen.svg" />
|
||||||
<Meta property="og:image" content="/social-share-zen.png" />
|
<Meta property="og:image" content="/social-share-zen.png" />
|
||||||
<Meta name="twitter:image" content="/social-share-zen.png" />
|
<Meta name="twitter:image" content="/social-share-zen.png" />
|
||||||
|
<Meta name="opencode:auth" content={loggedin() ? "true" : "false"} />
|
||||||
|
|
||||||
<div data-component="container">
|
<div data-component="container">
|
||||||
<Header zen />
|
<Header zen />
|
||||||
@@ -81,6 +83,9 @@ export default function Home() {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<IconGemini width="24" height="24" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path
|
<path
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Resource, waitUntil } from "@opencode-ai/console-resource"
|
||||||
|
|
||||||
|
export function createDataDumper(sessionId: string, requestId: string) {
|
||||||
|
if (Resource.App.stage !== "production") return
|
||||||
|
|
||||||
|
let data: Record<string, any> = {}
|
||||||
|
let modelName: string | undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
provideModel: (model?: string) => (modelName = model),
|
||||||
|
provideRequest: (request: string) => (data.request = request),
|
||||||
|
provideResponse: (response: string) => (data.response = response),
|
||||||
|
provideStream: (chunk: string) => (data.response = (data.response ?? "") + chunk),
|
||||||
|
flush: () => {
|
||||||
|
if (!modelName) return
|
||||||
|
|
||||||
|
const str = new Date().toISOString().replace(/[^0-9]/g, "")
|
||||||
|
const yyyymmdd = str.substring(0, 8)
|
||||||
|
const hh = str.substring(8, 10)
|
||||||
|
|
||||||
|
waitUntil(
|
||||||
|
Resource.ConsoleData.put(`${yyyymmdd}/${hh}/${modelName}/${sessionId}/${requestId}.json`, JSON.stringify(data)),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import { googleHelper } from "./provider/google"
|
|||||||
import { openaiHelper } from "./provider/openai"
|
import { openaiHelper } from "./provider/openai"
|
||||||
import { oaCompatHelper } from "./provider/openai-compatible"
|
import { oaCompatHelper } from "./provider/openai-compatible"
|
||||||
import { createRateLimiter } from "./rateLimiter"
|
import { createRateLimiter } from "./rateLimiter"
|
||||||
|
import { createDataDumper } from "./dataDumper"
|
||||||
|
|
||||||
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
|
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
|
||||||
type RetryOptions = {
|
type RetryOptions = {
|
||||||
@@ -48,16 +49,19 @@ export async function handler(
|
|||||||
try {
|
try {
|
||||||
const url = input.request.url
|
const url = input.request.url
|
||||||
const body = await input.request.json()
|
const body = await input.request.json()
|
||||||
const ip = input.request.headers.get("x-real-ip") ?? ""
|
|
||||||
const model = opts.parseModel(url, body)
|
const model = opts.parseModel(url, body)
|
||||||
const isStream = opts.parseIsStream(url, body)
|
const isStream = opts.parseIsStream(url, body)
|
||||||
|
const ip = input.request.headers.get("x-real-ip") ?? ""
|
||||||
|
const sessionId = input.request.headers.get("x-opencode-session") ?? ""
|
||||||
|
const requestId = input.request.headers.get("x-opencode-request") ?? ""
|
||||||
logger.metric({
|
logger.metric({
|
||||||
is_tream: isStream,
|
is_tream: isStream,
|
||||||
session: input.request.headers.get("x-opencode-session"),
|
session: sessionId,
|
||||||
request: input.request.headers.get("x-opencode-request"),
|
request: requestId,
|
||||||
})
|
})
|
||||||
const zenData = ZenData.list()
|
const zenData = ZenData.list()
|
||||||
const modelInfo = validateModel(zenData, model)
|
const modelInfo = validateModel(zenData, model)
|
||||||
|
const dataDumper = createDataDumper(sessionId, requestId)
|
||||||
const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
|
const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
|
||||||
await rateLimiter?.check()
|
await rateLimiter?.check()
|
||||||
|
|
||||||
@@ -104,10 +108,14 @@ export async function handler(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return { providerInfo, authInfo, res, startTimestamp }
|
return { providerInfo, authInfo, reqBody, res, startTimestamp }
|
||||||
}
|
}
|
||||||
|
|
||||||
const { providerInfo, authInfo, res, startTimestamp } = await retriableRequest()
|
const { providerInfo, authInfo, reqBody, res, startTimestamp } = await retriableRequest()
|
||||||
|
|
||||||
|
// Store model request
|
||||||
|
dataDumper?.provideModel(providerInfo.storeModel)
|
||||||
|
dataDumper?.provideRequest(reqBody)
|
||||||
|
|
||||||
// Scrub response headers
|
// Scrub response headers
|
||||||
const resHeaders = new Headers()
|
const resHeaders = new Headers()
|
||||||
@@ -126,6 +134,8 @@ export async function handler(
|
|||||||
const body = JSON.stringify(responseConverter(json))
|
const body = JSON.stringify(responseConverter(json))
|
||||||
logger.metric({ response_length: body.length })
|
logger.metric({ response_length: body.length })
|
||||||
logger.debug("RESPONSE: " + body)
|
logger.debug("RESPONSE: " + body)
|
||||||
|
dataDumper?.provideResponse(body)
|
||||||
|
dataDumper?.flush()
|
||||||
await rateLimiter?.track()
|
await rateLimiter?.track()
|
||||||
await trackUsage(authInfo, modelInfo, providerInfo, json.usage)
|
await trackUsage(authInfo, modelInfo, providerInfo, json.usage)
|
||||||
await reload(authInfo)
|
await reload(authInfo)
|
||||||
@@ -155,6 +165,7 @@ export async function handler(
|
|||||||
response_length: responseLength,
|
response_length: responseLength,
|
||||||
"timestamp.last_byte": Date.now(),
|
"timestamp.last_byte": Date.now(),
|
||||||
})
|
})
|
||||||
|
dataDumper?.flush()
|
||||||
await rateLimiter?.track()
|
await rateLimiter?.track()
|
||||||
const usage = usageParser.retrieve()
|
const usage = usageParser.retrieve()
|
||||||
if (usage) {
|
if (usage) {
|
||||||
@@ -174,6 +185,7 @@ export async function handler(
|
|||||||
}
|
}
|
||||||
responseLength += value.length
|
responseLength += value.length
|
||||||
buffer += decoder.decode(value, { stream: true })
|
buffer += decoder.decode(value, { stream: true })
|
||||||
|
dataDumper?.provideStream(buffer)
|
||||||
|
|
||||||
const parts = buffer.split(providerInfo.streamSeparator)
|
const parts = buffer.split(providerInfo.streamSeparator)
|
||||||
buffer = parts.pop() ?? ""
|
buffer = parts.pop() ?? ""
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"types": ["vinxi/types/client"],
|
"types": ["vite/client"],
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": ["./src/*"]
|
"~/*": ["./src/*"]
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { defineConfig, PluginOption } from "vite"
|
||||||
|
import { solidStart } from "@solidjs/start/config"
|
||||||
|
import { nitro } from "nitro/vite"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
solidStart() as PluginOption,
|
||||||
|
nitro({
|
||||||
|
compatibilityDate: "2024-09-19",
|
||||||
|
preset: "cloudflare_module",
|
||||||
|
cloudflare: {
|
||||||
|
nodeCompat: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
allowedHosts: true,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
external: ["cloudflare:workers"],
|
||||||
|
},
|
||||||
|
minify: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.0.85",
|
"version": "1.0.109",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
"update-models": "script/update-models.ts",
|
"update-models": "script/update-models.ts",
|
||||||
"promote-models-to-dev": "script/promote-models.ts dev",
|
"promote-models-to-dev": "script/promote-models.ts dev",
|
||||||
"promote-models-to-prod": "script/promote-models.ts production",
|
"promote-models-to-prod": "script/promote-models.ts production",
|
||||||
|
"pull-models-from-dev": "script/pull-models.ts dev",
|
||||||
"typecheck": "tsgo --noEmit"
|
"typecheck": "tsgo --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -11,20 +11,21 @@ const root = path.resolve(process.cwd(), "..", "..", "..")
|
|||||||
|
|
||||||
// read the secret
|
// read the secret
|
||||||
const ret = await $`bun sst secret list`.cwd(root).text()
|
const ret = await $`bun sst secret list`.cwd(root).text()
|
||||||
const value1 = ret
|
const lines = ret.split("\n")
|
||||||
.split("\n")
|
const value1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[1]
|
||||||
.find((line) => line.startsWith("ZEN_MODELS1"))
|
const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
|
||||||
?.split("=")[1]
|
const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
|
||||||
const value2 = ret
|
const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
||||||
.split("\n")
|
|
||||||
.find((line) => line.startsWith("ZEN_MODELS2"))
|
|
||||||
?.split("=")[1]
|
|
||||||
if (!value1) throw new Error("ZEN_MODELS1 not found")
|
if (!value1) throw new Error("ZEN_MODELS1 not found")
|
||||||
if (!value2) throw new Error("ZEN_MODELS2 not found")
|
if (!value2) throw new Error("ZEN_MODELS2 not found")
|
||||||
|
if (!value3) throw new Error("ZEN_MODELS3 not found")
|
||||||
|
if (!value4) throw new Error("ZEN_MODELS4 not found")
|
||||||
|
|
||||||
// validate value
|
// validate value
|
||||||
ZenData.validate(JSON.parse(value1 + value2))
|
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4))
|
||||||
|
|
||||||
// update the secret
|
// update the secret
|
||||||
await $`bun sst secret set ZEN_MODELS1 ${value1} --stage ${stage}`
|
await $`bun sst secret set ZEN_MODELS1 ${value1} --stage ${stage}`
|
||||||
await $`bun sst secret set ZEN_MODELS2 ${value2} --stage ${stage}`
|
await $`bun sst secret set ZEN_MODELS2 ${value2} --stage ${stage}`
|
||||||
|
await $`bun sst secret set ZEN_MODELS3 ${value3} --stage ${stage}`
|
||||||
|
await $`bun sst secret set ZEN_MODELS4 ${value4} --stage ${stage}`
|
||||||
|
|||||||
Executable
+31
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import { $ } from "bun"
|
||||||
|
import path from "path"
|
||||||
|
import { ZenData } from "../src/model"
|
||||||
|
|
||||||
|
const stage = process.argv[2]
|
||||||
|
if (!stage) throw new Error("Stage is required")
|
||||||
|
|
||||||
|
const root = path.resolve(process.cwd(), "..", "..", "..")
|
||||||
|
|
||||||
|
// read the secret
|
||||||
|
const ret = await $`bun sst secret list --stage ${stage}`.cwd(root).text()
|
||||||
|
const lines = ret.split("\n")
|
||||||
|
const value1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[1]
|
||||||
|
const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
|
||||||
|
const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
|
||||||
|
const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
||||||
|
if (!value1) throw new Error("ZEN_MODELS1 not found")
|
||||||
|
if (!value2) throw new Error("ZEN_MODELS2 not found")
|
||||||
|
if (!value3) throw new Error("ZEN_MODELS3 not found")
|
||||||
|
if (!value4) throw new Error("ZEN_MODELS4 not found")
|
||||||
|
|
||||||
|
// validate value
|
||||||
|
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4))
|
||||||
|
|
||||||
|
// update the secret
|
||||||
|
await $`bun sst secret set ZEN_MODELS1 ${value1}`
|
||||||
|
await $`bun sst secret set ZEN_MODELS2 ${value2}`
|
||||||
|
await $`bun sst secret set ZEN_MODELS3 ${value3}`
|
||||||
|
await $`bun sst secret set ZEN_MODELS4 ${value4}`
|
||||||
@@ -9,21 +9,20 @@ const root = path.resolve(process.cwd(), "..", "..", "..")
|
|||||||
const models = await $`bun sst secret list`.cwd(root).text()
|
const models = await $`bun sst secret list`.cwd(root).text()
|
||||||
|
|
||||||
// read the line starting with "ZEN_MODELS"
|
// read the line starting with "ZEN_MODELS"
|
||||||
const oldValue1 = models
|
const lines = models.split("\n")
|
||||||
.split("\n")
|
const oldValue1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[1]
|
||||||
.find((line) => line.startsWith("ZEN_MODELS1"))
|
const oldValue2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
|
||||||
?.split("=")[1]
|
const oldValue3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
|
||||||
const oldValue2 = models
|
const oldValue4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
||||||
.split("\n")
|
|
||||||
.find((line) => line.startsWith("ZEN_MODELS2"))
|
|
||||||
?.split("=")[1]
|
|
||||||
if (!oldValue1) throw new Error("ZEN_MODELS1 not found")
|
if (!oldValue1) throw new Error("ZEN_MODELS1 not found")
|
||||||
if (!oldValue2) throw new Error("ZEN_MODELS2 not found")
|
if (!oldValue2) throw new Error("ZEN_MODELS2 not found")
|
||||||
|
if (!oldValue3) throw new Error("ZEN_MODELS3 not found")
|
||||||
|
if (!oldValue4) throw new Error("ZEN_MODELS4 not found")
|
||||||
|
|
||||||
// store the prettified json to a temp file
|
// store the prettified json to a temp file
|
||||||
const filename = `models-${Date.now()}.json`
|
const filename = `models-${Date.now()}.json`
|
||||||
const tempFile = Bun.file(path.join(os.tmpdir(), filename))
|
const tempFile = Bun.file(path.join(os.tmpdir(), filename))
|
||||||
await tempFile.write(JSON.stringify(JSON.parse(oldValue1 + oldValue2), null, 2))
|
await tempFile.write(JSON.stringify(JSON.parse(oldValue1 + oldValue2 + oldValue3 + oldValue4), null, 2))
|
||||||
console.log("tempFile", tempFile.name)
|
console.log("tempFile", tempFile.name)
|
||||||
|
|
||||||
// open temp file in vim and read the file on close
|
// open temp file in vim and read the file on close
|
||||||
@@ -32,6 +31,12 @@ const newValue = JSON.stringify(JSON.parse(await tempFile.text()))
|
|||||||
ZenData.validate(JSON.parse(newValue))
|
ZenData.validate(JSON.parse(newValue))
|
||||||
|
|
||||||
// update the secret
|
// update the secret
|
||||||
const mid = Math.floor(newValue.length / 2)
|
const chunk = Math.ceil(newValue.length / 4)
|
||||||
await $`bun sst secret set ZEN_MODELS1 ${newValue.slice(0, mid)}`
|
const newValue1 = newValue.slice(0, chunk)
|
||||||
await $`bun sst secret set ZEN_MODELS2 ${newValue.slice(mid)}`
|
const newValue2 = newValue.slice(chunk, chunk * 2)
|
||||||
|
const newValue3 = newValue.slice(chunk * 2, chunk * 3)
|
||||||
|
const newValue4 = newValue.slice(chunk * 3)
|
||||||
|
await $`bun sst secret set ZEN_MODELS1 ${newValue1}`
|
||||||
|
await $`bun sst secret set ZEN_MODELS2 ${newValue2}`
|
||||||
|
await $`bun sst secret set ZEN_MODELS3 ${newValue3}`
|
||||||
|
await $`bun sst secret set ZEN_MODELS4 ${newValue4}`
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export namespace ZenData {
|
|||||||
model: z.string(),
|
model: z.string(),
|
||||||
weight: z.number().optional(),
|
weight: z.number().optional(),
|
||||||
disabled: z.boolean().optional(),
|
disabled: z.boolean().optional(),
|
||||||
|
storeModel: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
@@ -53,7 +54,9 @@ export namespace ZenData {
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const list = fn(z.void(), () => {
|
export const list = fn(z.void(), () => {
|
||||||
const json = JSON.parse(Resource.ZEN_MODELS1.value + Resource.ZEN_MODELS2.value)
|
const json = JSON.parse(
|
||||||
|
Resource.ZEN_MODELS1.value + Resource.ZEN_MODELS2.value + Resource.ZEN_MODELS3.value + Resource.ZEN_MODELS4.value,
|
||||||
|
)
|
||||||
return ModelsSchema.parse(json)
|
return ModelsSchema.parse(json)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+18
@@ -74,6 +74,14 @@ declare module "sst" {
|
|||||||
type: "sst.sst.Secret"
|
type: "sst.sst.Secret"
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
R2AccessKey: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
R2SecretKey: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
STRIPE_SECRET_KEY: {
|
STRIPE_SECRET_KEY: {
|
||||||
type: "sst.sst.Secret"
|
type: "sst.sst.Secret"
|
||||||
value: string
|
value: string
|
||||||
@@ -94,6 +102,14 @@ declare module "sst" {
|
|||||||
type: "sst.sst.Secret"
|
type: "sst.sst.Secret"
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
ZEN_MODELS3: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
ZEN_MODELS4: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// cloudflare
|
// cloudflare
|
||||||
@@ -104,6 +120,8 @@ declare module "sst" {
|
|||||||
AuthApi: cloudflare.Service
|
AuthApi: cloudflare.Service
|
||||||
AuthStorage: cloudflare.KVNamespace
|
AuthStorage: cloudflare.KVNamespace
|
||||||
Bucket: cloudflare.R2Bucket
|
Bucket: cloudflare.R2Bucket
|
||||||
|
ConsoleData: cloudflare.R2Bucket
|
||||||
|
EnterpriseStorage: cloudflare.R2Bucket
|
||||||
GatewayKv: cloudflare.KVNamespace
|
GatewayKv: cloudflare.KVNamespace
|
||||||
LogProcessor: cloudflare.Service
|
LogProcessor: cloudflare.Service
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.0.85",
|
"version": "1.0.109",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+18
@@ -74,6 +74,14 @@ declare module "sst" {
|
|||||||
type: "sst.sst.Secret"
|
type: "sst.sst.Secret"
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
R2AccessKey: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
R2SecretKey: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
STRIPE_SECRET_KEY: {
|
STRIPE_SECRET_KEY: {
|
||||||
type: "sst.sst.Secret"
|
type: "sst.sst.Secret"
|
||||||
value: string
|
value: string
|
||||||
@@ -94,6 +102,14 @@ declare module "sst" {
|
|||||||
type: "sst.sst.Secret"
|
type: "sst.sst.Secret"
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
ZEN_MODELS3: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
ZEN_MODELS4: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// cloudflare
|
// cloudflare
|
||||||
@@ -104,6 +120,8 @@ declare module "sst" {
|
|||||||
AuthApi: cloudflare.Service
|
AuthApi: cloudflare.Service
|
||||||
AuthStorage: cloudflare.KVNamespace
|
AuthStorage: cloudflare.KVNamespace
|
||||||
Bucket: cloudflare.R2Bucket
|
Bucket: cloudflare.R2Bucket
|
||||||
|
ConsoleData: cloudflare.R2Bucket
|
||||||
|
EnterpriseStorage: cloudflare.R2Bucket
|
||||||
GatewayKv: cloudflare.KVNamespace
|
GatewayKv: cloudflare.KVNamespace
|
||||||
LogProcessor: cloudflare.Service
|
LogProcessor: cloudflare.Service
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.0.85",
|
"version": "1.0.109",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { env } from "cloudflare:workers"
|
import { env } from "cloudflare:workers"
|
||||||
|
export { waitUntil } from "cloudflare:workers"
|
||||||
|
|
||||||
export const Resource = new Proxy(
|
export const Resource = new Proxy(
|
||||||
{},
|
{},
|
||||||
|
|||||||
@@ -2,54 +2,66 @@ import type { KVNamespaceListOptions, KVNamespaceListResult, KVNamespacePutOptio
|
|||||||
import { Resource as ResourceBase } from "sst"
|
import { Resource as ResourceBase } from "sst"
|
||||||
import Cloudflare from "cloudflare"
|
import Cloudflare from "cloudflare"
|
||||||
|
|
||||||
|
export const waitUntil = async (fn: () => Promise<void>) => {
|
||||||
|
await fn()
|
||||||
|
}
|
||||||
|
|
||||||
export const Resource = new Proxy(
|
export const Resource = new Proxy(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
get(_target, prop: keyof typeof ResourceBase) {
|
get(_target, prop: keyof typeof ResourceBase) {
|
||||||
const value = ResourceBase[prop]
|
const value = ResourceBase[prop]
|
||||||
// @ts-ignore
|
if ("type" in value) {
|
||||||
if ("type" in value && value.type === "sst.cloudflare.Kv") {
|
|
||||||
const client = new Cloudflare({
|
|
||||||
apiToken: ResourceBase.CLOUDFLARE_API_TOKEN.value,
|
|
||||||
})
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const namespaceId = value.namespaceId
|
if (value.type === "sst.cloudflare.Bucket") {
|
||||||
const accountId = ResourceBase.CLOUDFLARE_DEFAULT_ACCOUNT_ID.value
|
return {
|
||||||
return {
|
put: async () => {},
|
||||||
get: (k: string | string[]) => {
|
}
|
||||||
const isMulti = Array.isArray(k)
|
}
|
||||||
return client.kv.namespaces
|
// @ts-ignore
|
||||||
.bulkGet(namespaceId, {
|
if (value.type === "sst.cloudflare.Kv") {
|
||||||
keys: Array.isArray(k) ? k : [k],
|
const client = new Cloudflare({
|
||||||
|
apiToken: ResourceBase.CLOUDFLARE_API_TOKEN.value,
|
||||||
|
})
|
||||||
|
// @ts-ignore
|
||||||
|
const namespaceId = value.namespaceId
|
||||||
|
const accountId = ResourceBase.CLOUDFLARE_DEFAULT_ACCOUNT_ID.value
|
||||||
|
return {
|
||||||
|
get: (k: string | string[]) => {
|
||||||
|
const isMulti = Array.isArray(k)
|
||||||
|
return client.kv.namespaces
|
||||||
|
.bulkGet(namespaceId, {
|
||||||
|
keys: Array.isArray(k) ? k : [k],
|
||||||
|
account_id: accountId,
|
||||||
|
})
|
||||||
|
.then((result) => (isMulti ? new Map(Object.entries(result?.values ?? {})) : result?.values?.[k]))
|
||||||
|
},
|
||||||
|
put: (k: string, v: string, opts?: KVNamespacePutOptions) =>
|
||||||
|
client.kv.namespaces.values.update(namespaceId, k, {
|
||||||
account_id: accountId,
|
account_id: accountId,
|
||||||
})
|
value: v,
|
||||||
.then((result) => (isMulti ? new Map(Object.entries(result?.values ?? {})) : result?.values?.[k]))
|
expiration: opts?.expiration,
|
||||||
},
|
expiration_ttl: opts?.expirationTtl,
|
||||||
put: (k: string, v: string, opts?: KVNamespacePutOptions) =>
|
metadata: opts?.metadata,
|
||||||
client.kv.namespaces.values.update(namespaceId, k, {
|
|
||||||
account_id: accountId,
|
|
||||||
value: v,
|
|
||||||
expiration: opts?.expiration,
|
|
||||||
expiration_ttl: opts?.expirationTtl,
|
|
||||||
metadata: opts?.metadata,
|
|
||||||
}),
|
|
||||||
delete: (k: string) =>
|
|
||||||
client.kv.namespaces.values.delete(namespaceId, k, {
|
|
||||||
account_id: accountId,
|
|
||||||
}),
|
|
||||||
list: (opts?: KVNamespaceListOptions): Promise<KVNamespaceListResult<unknown, string>> =>
|
|
||||||
client.kv.namespaces.keys
|
|
||||||
.list(namespaceId, {
|
|
||||||
account_id: accountId,
|
|
||||||
prefix: opts?.prefix ?? undefined,
|
|
||||||
})
|
|
||||||
.then((result) => {
|
|
||||||
return {
|
|
||||||
keys: result.result,
|
|
||||||
list_complete: true,
|
|
||||||
cacheStatus: null,
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
|
delete: (k: string) =>
|
||||||
|
client.kv.namespaces.values.delete(namespaceId, k, {
|
||||||
|
account_id: accountId,
|
||||||
|
}),
|
||||||
|
list: (opts?: KVNamespaceListOptions): Promise<KVNamespaceListResult<unknown, string>> =>
|
||||||
|
client.kv.namespaces.keys
|
||||||
|
.list(namespaceId, {
|
||||||
|
account_id: accountId,
|
||||||
|
prefix: opts?.prefix ?? undefined,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
return {
|
||||||
|
keys: result.result,
|
||||||
|
list_complete: true,
|
||||||
|
cacheStatus: null,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
|
|||||||
+18
@@ -74,6 +74,14 @@ declare module "sst" {
|
|||||||
type: "sst.sst.Secret"
|
type: "sst.sst.Secret"
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
R2AccessKey: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
R2SecretKey: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
STRIPE_SECRET_KEY: {
|
STRIPE_SECRET_KEY: {
|
||||||
type: "sst.sst.Secret"
|
type: "sst.sst.Secret"
|
||||||
value: string
|
value: string
|
||||||
@@ -94,6 +102,14 @@ declare module "sst" {
|
|||||||
type: "sst.sst.Secret"
|
type: "sst.sst.Secret"
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
ZEN_MODELS3: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
ZEN_MODELS4: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// cloudflare
|
// cloudflare
|
||||||
@@ -104,6 +120,8 @@ declare module "sst" {
|
|||||||
AuthApi: cloudflare.Service
|
AuthApi: cloudflare.Service
|
||||||
AuthStorage: cloudflare.KVNamespace
|
AuthStorage: cloudflare.KVNamespace
|
||||||
Bucket: cloudflare.R2Bucket
|
Bucket: cloudflare.R2Bucket
|
||||||
|
ConsoleData: cloudflare.R2Bucket
|
||||||
|
EnterpriseStorage: cloudflare.R2Bucket
|
||||||
GatewayKv: cloudflare.KVNamespace
|
GatewayKv: cloudflare.KVNamespace
|
||||||
LogProcessor: cloudflare.Service
|
LogProcessor: cloudflare.Service
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.0.85",
|
"version": "1.0.109",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "catalog:",
|
"@tailwindcss/vite": "catalog:",
|
||||||
"@tsconfig/bun": "1.0.9",
|
"@tsconfig/bun": "1.0.9",
|
||||||
"@types/luxon": "3.7.1",
|
"@types/luxon": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@shikijs/transformers": "3.9.2",
|
"@shikijs/transformers": "3.9.2",
|
||||||
"@solid-primitives/active-element": "2.1.3",
|
"@solid-primitives/active-element": "2.1.3",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
@@ -33,7 +34,7 @@
|
|||||||
"@solid-primitives/scroll": "2.1.3",
|
"@solid-primitives/scroll": "2.1.3",
|
||||||
"@solid-primitives/storage": "4.3.3",
|
"@solid-primitives/storage": "4.3.3",
|
||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
"@solidjs/router": "0.15.3",
|
"@solidjs/router": "catalog:",
|
||||||
"@thisbeyond/solid-dnd": "0.7.5",
|
"@thisbeyond/solid-dnd": "0.7.5",
|
||||||
"diff": "catalog:",
|
"diff": "catalog:",
|
||||||
"fuzzysort": "catalog:",
|
"fuzzysort": "catalog:",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useLocal, type LocalFile } from "@/context/local"
|
import { useLocal, type LocalFile } from "@/context/local"
|
||||||
import { Tooltip } from "@opencode-ai/ui"
|
import { Collapsible } from "@/ui"
|
||||||
import { Collapsible, FileIcon } from "@/ui"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { For, Match, Switch, Show, type ComponentProps, type ParentProps } from "solid-js"
|
import { For, Match, Switch, Show, type ComponentProps, type ParentProps } from "solid-js"
|
||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { Button, Icon, IconButton, Select, SelectDialog, Tooltip } from "@opencode-ai/ui"
|
|
||||||
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||||
import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match } from "solid-js"
|
import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { FileIcon } from "@/ui"
|
|
||||||
import { getDirectory, getFilename } from "@/utils"
|
import { getDirectory, getFilename } from "@/utils"
|
||||||
import { createFocusSignal } from "@solid-primitives/active-element"
|
import { createFocusSignal } from "@solid-primitives/active-element"
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
@@ -11,6 +9,13 @@ import { ContentPart, DEFAULT_PROMPT, isPromptEqual, Prompt, useSession } from "
|
|||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
|
import { SelectDialog } from "@opencode-ai/ui/select-dialog"
|
||||||
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
|
import { Select } from "@opencode-ai/ui/select"
|
||||||
|
|
||||||
interface PromptInputProps {
|
interface PromptInputProps {
|
||||||
class?: string
|
class?: string
|
||||||
@@ -184,8 +189,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const range = selection.getRangeAt(0)
|
const range = selection.getRangeAt(0)
|
||||||
|
|
||||||
if (atMatch) {
|
if (atMatch) {
|
||||||
let node: Node | null = range.startContainer
|
// let node: Node | null = range.startContainer
|
||||||
let offset = range.startOffset
|
// let offset = range.startOffset
|
||||||
let runningLength = 0
|
let runningLength = 0
|
||||||
|
|
||||||
const walker = document.createTreeWalker(editorRef, NodeFilter.SHOW_TEXT, null)
|
const walker = document.createTreeWalker(editorRef, NodeFilter.SHOW_TEXT, null)
|
||||||
@@ -448,7 +453,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
{(i) => (
|
{(i) => (
|
||||||
<div class="w-full flex items-center justify-between gap-x-3">
|
<div class="w-full flex items-center justify-between gap-x-3">
|
||||||
<div class="flex items-center gap-x-2.5 text-text-muted grow min-w-0">
|
<div class="flex items-center gap-x-2.5 text-text-muted grow min-w-0">
|
||||||
<img src={`https://models.dev/logos/${i.provider.id}.svg`} class="size-6 p-0.5 shrink-0 " />
|
<img src={`https://models.dev/logos/${i.provider.id}.svg`} class="size-6 p-0.5 shrink-0" />
|
||||||
<div class="flex gap-x-3 items-baseline flex-[1_0_0]">
|
<div class="flex gap-x-3 items-baseline flex-[1_0_0]">
|
||||||
<span class="text-14-medium text-text-strong overflow-hidden text-ellipsis">{i.name}</span>
|
<span class="text-14-medium text-text-strong overflow-hidden text-ellipsis">{i.name}</span>
|
||||||
<Show when={i.release_date}>
|
<Show when={i.release_date}>
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
import { useSession } from "@/context/session"
|
|
||||||
import { FileIcon } from "@/ui"
|
|
||||||
import { getDirectory, getFilename } from "@/utils"
|
|
||||||
import { Accordion, Button, Diff, DiffChanges, Icon, IconButton, Tooltip } from "@opencode-ai/ui"
|
|
||||||
import { For, Match, Show, Switch } from "solid-js"
|
|
||||||
import { StickyAccordionHeader } from "./sticky-accordion-header"
|
|
||||||
import { createStore } from "solid-js/store"
|
|
||||||
import { useLayout } from "@/context/layout"
|
|
||||||
|
|
||||||
export const SessionReview = (props: { split?: boolean; class?: string; hideExpand?: boolean }) => {
|
|
||||||
const layout = useLayout()
|
|
||||||
const session = useSession()
|
|
||||||
const [store, setStore] = createStore({
|
|
||||||
open: session.diffs().map((d) => d.file),
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleChange = (open: string[]) => {
|
|
||||||
setStore("open", open)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleExpandOrCollapseAll = () => {
|
|
||||||
if (store.open.length > 0) {
|
|
||||||
setStore("open", [])
|
|
||||||
} else {
|
|
||||||
setStore(
|
|
||||||
"open",
|
|
||||||
session.diffs().map((d) => d.file),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
classList={{
|
|
||||||
"flex flex-col gap-3 h-full overflow-y-auto no-scrollbar": true,
|
|
||||||
[props.class ?? ""]: !!props.class,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="sticky top-0 z-20 bg-background-stronger h-8 shrink-0 flex justify-between items-center self-stretch">
|
|
||||||
<div class="text-14-medium text-text-strong">Session changes</div>
|
|
||||||
<div class="flex items-center gap-x-4 pr-px">
|
|
||||||
<Button size="normal" icon="chevron-grabber-vertical" onClick={handleExpandOrCollapseAll}>
|
|
||||||
<Switch>
|
|
||||||
<Match when={store.open.length > 0}>Collapse all</Match>
|
|
||||||
<Match when={true}>Expand all</Match>
|
|
||||||
</Switch>
|
|
||||||
</Button>
|
|
||||||
<Show when={!props.hideExpand}>
|
|
||||||
<Tooltip value="Open in tab">
|
|
||||||
<IconButton
|
|
||||||
icon="expand"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => {
|
|
||||||
layout.review.tab()
|
|
||||||
session.layout.setActiveTab("review")
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Accordion multiple value={store.open} onChange={handleChange}>
|
|
||||||
<For each={session.diffs()}>
|
|
||||||
{(diff) => (
|
|
||||||
<Accordion.Item value={diff.file}>
|
|
||||||
<StickyAccordionHeader class="top-11 data-expanded:before:-top-11">
|
|
||||||
<Accordion.Trigger class="bg-background-stronger">
|
|
||||||
<div class="flex items-center justify-between w-full gap-5">
|
|
||||||
<div class="grow flex items-center gap-5 min-w-0">
|
|
||||||
<FileIcon node={{ path: diff.file, type: "file" }} class="shrink-0 size-4" />
|
|
||||||
<div class="flex grow min-w-0">
|
|
||||||
<Show when={diff.file.includes("/")}>
|
|
||||||
<span class="text-text-base truncate-start">{getDirectory(diff.file)}‎</span>
|
|
||||||
</Show>
|
|
||||||
<span class="text-text-strong shrink-0">{getFilename(diff.file)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="shrink-0 flex gap-4 items-center justify-end">
|
|
||||||
<DiffChanges changes={diff} />
|
|
||||||
<Icon name="chevron-grabber-vertical" size="small" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Accordion.Trigger>
|
|
||||||
</StickyAccordionHeader>
|
|
||||||
<Accordion.Content>
|
|
||||||
<Diff
|
|
||||||
diffStyle={props.split ? "split" : "unified"}
|
|
||||||
before={{
|
|
||||||
name: diff.file!,
|
|
||||||
contents: diff.before!,
|
|
||||||
}}
|
|
||||||
after={{
|
|
||||||
name: diff.file!,
|
|
||||||
contents: diff.after!,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Accordion.Content>
|
|
||||||
</Accordion.Item>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</Accordion>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { Accordion } from "@opencode-ai/ui"
|
|
||||||
import { ParentProps } from "solid-js"
|
|
||||||
|
|
||||||
export function StickyAccordionHeader(props: ParentProps<{ class?: string }>) {
|
|
||||||
return (
|
|
||||||
<Accordion.Header
|
|
||||||
classList={{
|
|
||||||
"sticky top-0 data-expanded:z-10": true,
|
|
||||||
"data-expanded:before:content-[''] data-expanded:before:z-[-10]": true,
|
|
||||||
"data-expanded:before:absolute data-expanded:before:inset-0 data-expanded:before:bg-background-stronger": true,
|
|
||||||
[props.class ?? ""]: !!props.class,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</Accordion.Header>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/client"
|
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/client"
|
||||||
import { createSimpleContext } from "./helper"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||||
import { onCleanup } from "solid-js"
|
import { onCleanup } from "solid-js"
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import type {
|
|||||||
SessionStatus,
|
SessionStatus,
|
||||||
} from "@opencode-ai/sdk"
|
} from "@opencode-ai/sdk"
|
||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
import { Binary } from "@/utils/binary"
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
import { createSimpleContext } from "./helper"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { useGlobalSDK } from "./global-sdk"
|
import { useGlobalSDK } from "./global-sdk"
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { createContext, Show, useContext, type ParentProps } from "solid-js"
|
|
||||||
|
|
||||||
export function createSimpleContext<T, Props extends Record<string, any>>(input: {
|
|
||||||
name: string
|
|
||||||
init: ((input: Props) => T) | (() => T)
|
|
||||||
}) {
|
|
||||||
const ctx = createContext<T>()
|
|
||||||
|
|
||||||
return {
|
|
||||||
provider: (props: ParentProps<Props>) => {
|
|
||||||
const init = input.init(props)
|
|
||||||
return (
|
|
||||||
// @ts-expect-error
|
|
||||||
<Show when={init.ready === undefined || init.ready === true}>
|
|
||||||
<ctx.Provider value={init}>{props.children}</ctx.Provider>
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
use() {
|
|
||||||
const value = useContext(ctx)
|
|
||||||
if (!value) throw new Error(`${input.name} context must be used within a context provider`)
|
|
||||||
return value
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { createMemo } from "solid-js"
|
import { createMemo } from "solid-js"
|
||||||
import { createSimpleContext } from "./helper"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { makePersisted } from "@solid-primitives/storage"
|
import { makePersisted } from "@solid-primitives/storage"
|
||||||
import { useGlobalSync } from "./global-sync"
|
import { useGlobalSync } from "./global-sync"
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createStore, produce, reconcile } from "solid-js/store"
|
|||||||
import { batch, createEffect, createMemo } from "solid-js"
|
import { batch, createEffect, createMemo } from "solid-js"
|
||||||
import { uniqueBy } from "remeda"
|
import { uniqueBy } from "remeda"
|
||||||
import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk"
|
import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk"
|
||||||
import { createSimpleContext } from "./helper"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { useSDK } from "./sdk"
|
import { useSDK } from "./sdk"
|
||||||
import { useSync } from "./sync"
|
import { useSync } from "./sync"
|
||||||
import { base64Encode } from "@/utils"
|
import { base64Encode } from "@/utils"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/client"
|
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/client"
|
||||||
import { createSimpleContext } from "./helper"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||||
import { onCleanup } from "solid-js"
|
import { onCleanup } from "solid-js"
|
||||||
import { useGlobalSDK } from "./global-sdk"
|
import { useGlobalSDK } from "./global-sdk"
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { createSimpleContext } from "./helper"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { batch, createEffect, createMemo } from "solid-js"
|
import { batch, createEffect, createMemo } from "solid-js"
|
||||||
import { useSync } from "./sync"
|
import { useSync } from "./sync"
|
||||||
import { makePersisted } from "@solid-primitives/storage"
|
import { makePersisted } from "@solid-primitives/storage"
|
||||||
import { TextSelection } from "./local"
|
import { TextSelection } from "./local"
|
||||||
import { pipe, sumBy } from "remeda"
|
import { pipe, sumBy } from "remeda"
|
||||||
import { AssistantMessage } from "@opencode-ai/sdk"
|
import { AssistantMessage, UserMessage } from "@opencode-ai/sdk"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { base64Encode } from "@/utils"
|
import { base64Encode } from "@/utils"
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
|
|||||||
})
|
})
|
||||||
const status = createMemo(
|
const status = createMemo(
|
||||||
() =>
|
() =>
|
||||||
sync.data.session_status[params.id] ?? {
|
sync.data.session_status[params.id ?? ""] ?? {
|
||||||
type: "idle",
|
type: "idle",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -123,8 +123,8 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
|
|||||||
user: userMessages,
|
user: userMessages,
|
||||||
last: lastUserMessage,
|
last: lastUserMessage,
|
||||||
active: activeMessage,
|
active: activeMessage,
|
||||||
setActive(id: string | undefined) {
|
setActive(message: UserMessage | undefined) {
|
||||||
setStore("messageId", id)
|
setStore("messageId", message?.id)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
usage: {
|
usage: {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { Part } from "@opencode-ai/sdk"
|
import type { Part } from "@opencode-ai/sdk"
|
||||||
import { produce } from "solid-js/store"
|
import { produce } from "solid-js/store"
|
||||||
import { createMemo } from "solid-js"
|
import { createMemo } from "solid-js"
|
||||||
import { Binary } from "@/utils/binary"
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
import { createSimpleContext } from "./helper"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { useGlobalSync } from "./global-sync"
|
import { useGlobalSync } from "./global-sync"
|
||||||
import { useSDK } from "./sdk"
|
import { useSDK } from "./sdk"
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import "@/index.css"
|
|||||||
import { render } from "solid-js/web"
|
import { render } from "solid-js/web"
|
||||||
import { Router, Route, Navigate } from "@solidjs/router"
|
import { Router, Route, Navigate } from "@solidjs/router"
|
||||||
import { MetaProvider } from "@solidjs/meta"
|
import { MetaProvider } from "@solidjs/meta"
|
||||||
import { Fonts, MarkedProvider } from "@opencode-ai/ui"
|
import { Fonts } from "@opencode-ai/ui/fonts"
|
||||||
|
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
|
||||||
import { GlobalSyncProvider, useGlobalSync } from "./context/global-sync"
|
import { GlobalSyncProvider, useGlobalSync } from "./context/global-sync"
|
||||||
import Layout from "@/pages/layout"
|
import Layout from "@/pages/layout"
|
||||||
import DirectoryLayout from "@/pages/directory-layout"
|
import DirectoryLayout from "@/pages/directory-layout"
|
||||||
|
|||||||
@@ -1,22 +1,31 @@
|
|||||||
import { createMemo, type ParentProps } from "solid-js"
|
import { createMemo, type ParentProps } from "solid-js"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { SDKProvider } from "@/context/sdk"
|
import { SDKProvider } from "@/context/sdk"
|
||||||
import { SyncProvider } from "@/context/sync"
|
import { SyncProvider, useSync } from "@/context/sync"
|
||||||
import { LocalProvider } from "@/context/local"
|
import { LocalProvider } from "@/context/local"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { base64Decode } from "@/utils"
|
import { base64Decode } from "@/utils"
|
||||||
|
import { DataProvider } from "@opencode-ai/ui/context"
|
||||||
|
import { iife } from "@opencode-ai/util/iife"
|
||||||
|
|
||||||
export default function Layout(props: ParentProps) {
|
export default function Layout(props: ParentProps) {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const sync = useGlobalSync()
|
const sync = useGlobalSync()
|
||||||
const directory = createMemo(() => {
|
const directory = createMemo(() => {
|
||||||
const decoded = base64Decode(params.dir)
|
const decoded = base64Decode(params.dir!)
|
||||||
return sync.data.projects.find((x) => x.worktree === decoded)?.worktree ?? "/"
|
return sync.data.projects.find((x) => x.worktree === decoded)?.worktree ?? "/"
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<SDKProvider directory={directory()}>
|
<SDKProvider directory={directory()}>
|
||||||
<SyncProvider>
|
<SyncProvider>
|
||||||
<LocalProvider>{props.children}</LocalProvider>
|
{iife(() => {
|
||||||
|
const sync = useSync()
|
||||||
|
return (
|
||||||
|
<DataProvider data={sync.data}>
|
||||||
|
<LocalProvider>{props.children}</LocalProvider>
|
||||||
|
</DataProvider>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</SyncProvider>
|
</SyncProvider>
|
||||||
</SDKProvider>
|
</SDKProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useGlobalSync } from "@/context/global-sync"
|
|||||||
import { base64Encode, getFilename } from "@/utils"
|
import { base64Encode, getFilename } from "@/utils"
|
||||||
import { For } from "solid-js"
|
import { For } from "solid-js"
|
||||||
import { A } from "@solidjs/router"
|
import { A } from "@solidjs/router"
|
||||||
import { Button } from "@opencode-ai/ui"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const sync = useGlobalSync()
|
const sync = useGlobalSync()
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import { Button, Tooltip, DiffChanges, IconButton, Mark, Icon, Collapsible } from "@opencode-ai/ui"
|
|
||||||
import { createMemo, For, ParentProps, Show } from "solid-js"
|
import { createMemo, For, ParentProps, Show } from "solid-js"
|
||||||
import { DateTime } from "luxon"
|
import { DateTime } from "luxon"
|
||||||
import { A, useParams } from "@solidjs/router"
|
import { A, useParams } from "@solidjs/router"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { base64Encode, getFilename } from "@/utils"
|
import { base64Encode, getFilename } from "@/utils"
|
||||||
|
import { Mark } from "@opencode-ai/ui/logo"
|
||||||
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
|
import { Collapsible } from "@opencode-ai/ui/collapsible"
|
||||||
|
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
||||||
|
|
||||||
export default function Layout(props: ParentProps) {
|
export default function Layout(props: ParentProps) {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
|
|||||||
@@ -1,38 +1,21 @@
|
|||||||
import {
|
import { For, onCleanup, onMount, Show, Match, Switch, createResource, createMemo } from "solid-js"
|
||||||
SelectDialog,
|
|
||||||
IconButton,
|
|
||||||
Tabs,
|
|
||||||
Icon,
|
|
||||||
Accordion,
|
|
||||||
Diff,
|
|
||||||
Collapsible,
|
|
||||||
DiffChanges,
|
|
||||||
Message,
|
|
||||||
Typewriter,
|
|
||||||
Card,
|
|
||||||
Code,
|
|
||||||
Tooltip,
|
|
||||||
ProgressCircle,
|
|
||||||
} from "@opencode-ai/ui"
|
|
||||||
import { FileIcon } from "@/ui"
|
|
||||||
import { MessageProgress } from "@/components/message-progress"
|
|
||||||
import {
|
|
||||||
For,
|
|
||||||
onCleanup,
|
|
||||||
onMount,
|
|
||||||
Show,
|
|
||||||
Match,
|
|
||||||
Switch,
|
|
||||||
createSignal,
|
|
||||||
createEffect,
|
|
||||||
createMemo,
|
|
||||||
createResource,
|
|
||||||
} from "solid-js"
|
|
||||||
import { useLocal, type LocalFile } from "@/context/local"
|
import { useLocal, type LocalFile } from "@/context/local"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { getDirectory, getFilename } from "@/utils"
|
import { getDirectory, getFilename } from "@/utils"
|
||||||
import { PromptInput } from "@/components/prompt-input"
|
import { PromptInput } from "@/components/prompt-input"
|
||||||
import { DateTime } from "luxon"
|
import { DateTime } from "luxon"
|
||||||
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
|
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
||||||
|
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
|
||||||
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
|
import { Code } from "@opencode-ai/ui/code"
|
||||||
|
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
||||||
|
import { MessageNav } from "@opencode-ai/ui/message-nav"
|
||||||
|
import { SessionReview } from "@opencode-ai/ui/session-review"
|
||||||
|
import { SelectDialog } from "@opencode-ai/ui/select-dialog"
|
||||||
import {
|
import {
|
||||||
DragDropProvider,
|
DragDropProvider,
|
||||||
DragDropSensors,
|
DragDropSensors,
|
||||||
@@ -45,14 +28,8 @@ import {
|
|||||||
import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
|
import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
|
||||||
import type { JSX } from "solid-js"
|
import type { JSX } from "solid-js"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { type AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk"
|
|
||||||
import { Markdown } from "@opencode-ai/ui"
|
|
||||||
import { Spinner } from "@/components/spinner"
|
|
||||||
import { useSession } from "@/context/session"
|
import { useSession } from "@/context/session"
|
||||||
import { StickyAccordionHeader } from "@/components/sticky-accordion-header"
|
|
||||||
import { SessionReview } from "@/components/session-review"
|
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { createSessionSeen } from "@/hooks/create-session-seen"
|
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
@@ -65,7 +42,6 @@ export default function Page() {
|
|||||||
activeDraggable: undefined as string | undefined,
|
activeDraggable: undefined as string | undefined,
|
||||||
})
|
})
|
||||||
let inputRef!: HTMLDivElement
|
let inputRef!: HTMLDivElement
|
||||||
let messageScrollElement!: HTMLDivElement
|
|
||||||
|
|
||||||
const MOD = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) ? "Meta" : "Control"
|
const MOD = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) ? "Meta" : "Control"
|
||||||
|
|
||||||
@@ -280,6 +256,8 @@ export default function Page() {
|
|||||||
return typeof draggable.id === "string" ? draggable.id : undefined
|
return typeof draggable.id === "string" ? draggable.id : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const wide = createMemo(() => layout.review.state() === "tab" || !session.diffs().length)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="relative bg-background-base size-full overflow-x-hidden">
|
<div class="relative bg-background-base size-full overflow-x-hidden">
|
||||||
<DragDropProvider
|
<DragDropProvider
|
||||||
@@ -355,286 +333,39 @@ export default function Page() {
|
|||||||
flex: layout.review.state() === "pane",
|
flex: layout.review.state() === "pane",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="relative shrink-0 px-6 py-3 flex flex-col gap-6 flex-1 min-h-0 w-full max-w-xl mx-auto">
|
<div class="relative shrink-0 px-6 py-3 flex flex-col gap-6 flex-1 min-h-0 w-full max-w-2xl mx-auto">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={session.id}>
|
<Match when={session.id}>
|
||||||
<div
|
<div class="flex items-start justify-start h-full min-h-0">
|
||||||
classList={{
|
|
||||||
"flex-1 min-h-0 pb-20": true,
|
|
||||||
"flex items-start justify-start": layout.review.state() === "pane",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Show when={session.messages.user().length > 1}>
|
<Show when={session.messages.user().length > 1}>
|
||||||
{(_) => {
|
<>
|
||||||
const expanded = createMemo(() => layout.review.state() === "tab" || !session.diffs().length)
|
<MessageNav
|
||||||
|
class="@6xl:hidden mt-3 mr-8"
|
||||||
return (
|
messages={session.messages.user()}
|
||||||
<ul
|
current={session.messages.active()}
|
||||||
role="list"
|
onMessageSelect={session.messages.setActive}
|
||||||
classList={{
|
size="compact"
|
||||||
"mr-8 shrink-0 flex flex-col items-start": true,
|
working={session.working()}
|
||||||
"absolute right-full w-60 mt-3 @7xl:gap-2 @7xl:mt-1": expanded(),
|
/>
|
||||||
"mt-3": !expanded(),
|
<MessageNav
|
||||||
}}
|
classList={{
|
||||||
>
|
"hidden @6xl:block": true,
|
||||||
<For each={session.messages.user()}>
|
"mt-0.5 mr-3 absolute right-full": wide(),
|
||||||
{(message) => {
|
"mt-3 mr-8": !wide(),
|
||||||
const working = createMemo(
|
}}
|
||||||
() => message.id === session.messages.last()?.id && session.working(),
|
messages={session.messages.user()}
|
||||||
)
|
current={session.messages.active()}
|
||||||
const handleClick = () => session.messages.setActive(message.id)
|
onMessageSelect={session.messages.setActive}
|
||||||
|
size={wide() ? "normal" : "compact"}
|
||||||
return (
|
working={session.working()}
|
||||||
<li
|
/>
|
||||||
classList={{
|
</>
|
||||||
"group/li flex items-center self-stretch justify-end": true,
|
|
||||||
"@7xl:justify-start": expanded(),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tooltip
|
|
||||||
placement="right"
|
|
||||||
gutter={8}
|
|
||||||
value={
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DiffChanges changes={message.summary?.diffs ?? []} variant="bars" />
|
|
||||||
{message.summary?.title}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
data-active={session.messages.active()?.id === message.id}
|
|
||||||
onClick={handleClick}
|
|
||||||
classList={{
|
|
||||||
"group/tick flex items-center justify-start h-2 w-8 -mr-3": true,
|
|
||||||
"data-[active=true]:[&>div]:bg-icon-strong-base data-[active=true]:[&>div]:w-full": true,
|
|
||||||
"@7xl:hidden": expanded(),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="h-px w-5 bg-icon-base group-hover/tick:w-full group-hover/tick:bg-icon-strong-base" />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
<button
|
|
||||||
classList={{
|
|
||||||
"hidden items-center self-stretch w-full gap-x-2 cursor-default": true,
|
|
||||||
"@7xl:flex": expanded(),
|
|
||||||
}}
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
<Switch>
|
|
||||||
<Match when={working()}>
|
|
||||||
<Spinner class="text-text-base shrink-0 w-[18px] aspect-square" />
|
|
||||||
</Match>
|
|
||||||
<Match when={true}>
|
|
||||||
<DiffChanges changes={message.summary?.diffs ?? []} variant="bars" />
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
<div
|
|
||||||
data-active={session.messages.active()?.id === message.id}
|
|
||||||
classList={{
|
|
||||||
"text-14-regular text-text-weak whitespace-nowrap truncate min-w-0": true,
|
|
||||||
"text-text-weak data-[active=true]:text-text-strong group-hover/li:text-text-base": true,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Show when={message.summary?.title} fallback="New message">
|
|
||||||
{message.summary?.title}
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</ul>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Show>
|
</Show>
|
||||||
<div ref={messageScrollElement} class="grow size-full min-w-0 overflow-y-auto no-scrollbar">
|
<SessionTurn
|
||||||
<For each={session.messages.user()}>
|
sessionID={session.id!}
|
||||||
{(message) => {
|
messageID={session.messages.active()?.id!}
|
||||||
const isActive = createMemo(() => session.messages.active()?.id === message.id)
|
classes={{ root: "pb-20 flex-1 min-w-0", content: "pb-20" }}
|
||||||
const titleSeen = createSessionSeen(`message-title-${message.id}`)
|
/>
|
||||||
const contentSeen = createSessionSeen(`message-content-${message.id}`)
|
|
||||||
const [titled, setTitled] = createSignal(titleSeen())
|
|
||||||
const assistantMessages = createMemo(() => {
|
|
||||||
if (!session.id) return []
|
|
||||||
return sync.data.message[session.id]?.filter(
|
|
||||||
(m) => m.role === "assistant" && m.parentID == message.id,
|
|
||||||
) as AssistantMessageType[]
|
|
||||||
})
|
|
||||||
const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
|
|
||||||
const [detailsExpanded, setDetailsExpanded] = createSignal(false)
|
|
||||||
const parts = createMemo(() => sync.data.part[message.id])
|
|
||||||
const hasToolPart = createMemo(() =>
|
|
||||||
assistantMessages()
|
|
||||||
?.flatMap((m) => sync.data.part[m.id])
|
|
||||||
.some((p) => p?.type === "tool"),
|
|
||||||
)
|
|
||||||
const working = createMemo(
|
|
||||||
() => message.id === session.messages.last()?.id && session.working(),
|
|
||||||
)
|
|
||||||
const initialCompleted = !(message.id === session.messages.last()?.id && session.working())
|
|
||||||
const [completed, setCompleted] = createSignal(initialCompleted)
|
|
||||||
|
|
||||||
// allowing time for the animations to finish
|
|
||||||
createEffect(() => {
|
|
||||||
if (titleSeen()) return
|
|
||||||
const title = message.summary?.title
|
|
||||||
if (title) setTimeout(() => setTitled(true), 10_000)
|
|
||||||
})
|
|
||||||
createEffect(() => {
|
|
||||||
const completed = !working()
|
|
||||||
setTimeout(() => setCompleted(completed), 1200)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Show when={isActive()}>
|
|
||||||
<div
|
|
||||||
data-message={message.id}
|
|
||||||
class="flex flex-col items-start self-stretch gap-8 pb-20"
|
|
||||||
>
|
|
||||||
{/* Title */}
|
|
||||||
<div class="flex items-center gap-2 self-stretch sticky top-0 bg-background-stronger z-20 h-8">
|
|
||||||
<div class="w-full text-14-medium text-text-strong">
|
|
||||||
<Show
|
|
||||||
when={titled()}
|
|
||||||
fallback={
|
|
||||||
<Typewriter
|
|
||||||
as="h1"
|
|
||||||
text={message.summary?.title}
|
|
||||||
class="overflow-hidden text-ellipsis min-w-0 text-nowrap"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<h1 class="overflow-hidden text-ellipsis min-w-0 text-nowrap">
|
|
||||||
{message.summary?.title}
|
|
||||||
</h1>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Message message={message} parts={parts()} />
|
|
||||||
{/* Summary */}
|
|
||||||
<Show when={completed()}>
|
|
||||||
<div class="w-full flex flex-col gap-6 items-start self-stretch">
|
|
||||||
<div class="flex flex-col items-start gap-1 self-stretch">
|
|
||||||
<h2 class="text-12-medium text-text-weak">
|
|
||||||
<Switch>
|
|
||||||
<Match when={message.summary?.diffs?.length}>Summary</Match>
|
|
||||||
<Match when={true}>Response</Match>
|
|
||||||
</Switch>
|
|
||||||
</h2>
|
|
||||||
<Show when={message.summary?.body}>
|
|
||||||
{(summary) => (
|
|
||||||
<Markdown
|
|
||||||
classList={{
|
|
||||||
"text-14-regular": !!message.summary?.diffs?.length,
|
|
||||||
"[&>*]:fade-up-text": !message.summary?.diffs?.length && !contentSeen(),
|
|
||||||
}}
|
|
||||||
text={summary()}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<Accordion class="w-full" multiple>
|
|
||||||
<For each={message.summary?.diffs ?? []}>
|
|
||||||
{(diff) => (
|
|
||||||
<Accordion.Item value={diff.file}>
|
|
||||||
<StickyAccordionHeader class="top-10 data-expanded:before:-top-10">
|
|
||||||
<Accordion.Trigger>
|
|
||||||
<div class="flex items-center justify-between w-full gap-5">
|
|
||||||
<div class="grow flex items-center gap-5 min-w-0">
|
|
||||||
<FileIcon
|
|
||||||
node={{ path: diff.file, type: "file" }}
|
|
||||||
class="shrink-0 size-4"
|
|
||||||
/>
|
|
||||||
<div class="flex grow min-w-0">
|
|
||||||
<Show when={diff.file.includes("/")}>
|
|
||||||
<span class="text-text-base truncate-start">
|
|
||||||
{getDirectory(diff.file)}‎
|
|
||||||
</span>
|
|
||||||
</Show>
|
|
||||||
<span class="text-text-strong shrink-0">
|
|
||||||
{getFilename(diff.file)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="shrink-0 flex gap-4 items-center justify-end">
|
|
||||||
<DiffChanges changes={diff} />
|
|
||||||
<Icon name="chevron-grabber-vertical" size="small" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Accordion.Trigger>
|
|
||||||
</StickyAccordionHeader>
|
|
||||||
<Accordion.Content class="max-h-60 overflow-y-auto no-scrollbar">
|
|
||||||
<Diff
|
|
||||||
before={{
|
|
||||||
name: diff.file!,
|
|
||||||
contents: diff.before!,
|
|
||||||
}}
|
|
||||||
after={{
|
|
||||||
name: diff.file!,
|
|
||||||
contents: diff.after!,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Accordion.Content>
|
|
||||||
</Accordion.Item>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</Accordion>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Show when={error() && !detailsExpanded()}>
|
|
||||||
<Card variant="error" class="text-text-on-critical-base">
|
|
||||||
{error()?.data?.message as string}
|
|
||||||
</Card>
|
|
||||||
</Show>
|
|
||||||
{/* Response */}
|
|
||||||
<div class="w-full">
|
|
||||||
<Switch>
|
|
||||||
<Match when={!completed()}>
|
|
||||||
<MessageProgress assistantMessages={assistantMessages} done={!working()} />
|
|
||||||
</Match>
|
|
||||||
<Match when={completed() && hasToolPart()}>
|
|
||||||
<Collapsible
|
|
||||||
variant="ghost"
|
|
||||||
open={detailsExpanded()}
|
|
||||||
onOpenChange={setDetailsExpanded}
|
|
||||||
>
|
|
||||||
<Collapsible.Trigger class="text-text-weak hover:text-text-strong">
|
|
||||||
<div class="flex items-center gap-1 self-stretch">
|
|
||||||
<div class="text-12-medium">
|
|
||||||
<Switch>
|
|
||||||
<Match when={detailsExpanded()}>Hide details</Match>
|
|
||||||
<Match when={!detailsExpanded()}>Show details</Match>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
<Collapsible.Arrow />
|
|
||||||
</div>
|
|
||||||
</Collapsible.Trigger>
|
|
||||||
<Collapsible.Content>
|
|
||||||
<div class="w-full flex flex-col items-start self-stretch gap-3">
|
|
||||||
<For each={assistantMessages()}>
|
|
||||||
{(assistantMessage) => {
|
|
||||||
const parts = createMemo(() => sync.data.part[assistantMessage.id])
|
|
||||||
return <Message message={assistantMessage} parts={parts()} />
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
<Show when={error()}>
|
|
||||||
<Card variant="error" class="text-text-on-critical-base">
|
|
||||||
{error()?.data?.message as string}
|
|
||||||
</Card>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Collapsible.Content>
|
|
||||||
</Collapsible>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
@@ -673,7 +404,21 @@ export default function Page() {
|
|||||||
"relative grow px-6 py-3 flex-1 min-h-0 border-l border-border-weak-base": true,
|
"relative grow px-6 py-3 flex-1 min-h-0 border-l border-border-weak-base": true,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SessionReview />
|
<SessionReview
|
||||||
|
diffs={session.diffs()}
|
||||||
|
actions={
|
||||||
|
<Tooltip value="Open in tab">
|
||||||
|
<IconButton
|
||||||
|
icon="expand"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
layout.review.tab()
|
||||||
|
session.layout.setActiveTab("review")
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
@@ -685,7 +430,7 @@ export default function Page() {
|
|||||||
"relative px-6 py-3 flex-1 min-h-0 overflow-hidden": true,
|
"relative px-6 py-3 flex-1 min-h-0 overflow-hidden": true,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SessionReview split hideExpand class="pb-40" />
|
<SessionReview diffs={session.diffs()} split class="pb-40" />
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -740,7 +485,7 @@ export default function Page() {
|
|||||||
</DragOverlay>
|
</DragOverlay>
|
||||||
</DragDropProvider>
|
</DragDropProvider>
|
||||||
<Show when={session.layout.tabs.active}>
|
<Show when={session.layout.tabs.active}>
|
||||||
<div class="absolute inset-x-0 px-6 max-w-2xl flex flex-col justify-center items-center z-50 mx-auto bottom-6">
|
<div class="absolute inset-x-0 px-6 max-w-2xl flex flex-col justify-center items-center z-50 mx-auto bottom-8">
|
||||||
<PromptInput
|
<PromptInput
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
inputRef = el
|
inputRef = el
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Collapsible as KobalteCollapsible } from "@kobalte/core/collapsible"
|
import { Collapsible as KobalteCollapsible } from "@kobalte/core/collapsible"
|
||||||
|
import { Icon, IconProps } from "@opencode-ai/ui/icon"
|
||||||
import { splitProps } from "solid-js"
|
import { splitProps } from "solid-js"
|
||||||
import type { ComponentProps, ParentProps } from "solid-js"
|
import type { ComponentProps, ParentProps } from "solid-js"
|
||||||
import { Icon, type IconProps } from "@opencode-ai/ui"
|
|
||||||
|
|
||||||
export interface CollapsibleProps extends ComponentProps<typeof KobalteCollapsible> {}
|
export interface CollapsibleProps extends ComponentProps<typeof KobalteCollapsible> {}
|
||||||
export interface CollapsibleTriggerProps extends ComponentProps<typeof KobalteCollapsible.Trigger> {}
|
export interface CollapsibleTriggerProps extends ComponentProps<typeof KobalteCollapsible.Trigger> {}
|
||||||
|
|||||||
@@ -4,4 +4,3 @@ export {
|
|||||||
type CollapsibleTriggerProps,
|
type CollapsibleTriggerProps,
|
||||||
type CollapsibleContentProps,
|
type CollapsibleContentProps,
|
||||||
} from "./collapsible"
|
} from "./collapsible"
|
||||||
export { FileIcon, type FileIconProps } from "./file-icon"
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { defineConfig } from "vite"
|
|||||||
import solidPlugin from "vite-plugin-solid"
|
import solidPlugin from "vite-plugin-solid"
|
||||||
import tailwindcss from "@tailwindcss/vite"
|
import tailwindcss from "@tailwindcss/vite"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { iconsSpritesheet } from "vite-plugin-icons-spritesheet"
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
resolve: {
|
resolve: {
|
||||||
@@ -10,18 +9,10 @@ export default defineConfig({
|
|||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [tailwindcss(), solidPlugin()] as any,
|
||||||
tailwindcss(),
|
|
||||||
solidPlugin(),
|
|
||||||
iconsSpritesheet({
|
|
||||||
withTypes: true,
|
|
||||||
inputDir: "src/assets/file-icons",
|
|
||||||
outputDir: "src/ui/file-icons",
|
|
||||||
formatter: "prettier",
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
|
allowedHosts: true,
|
||||||
port: 3000,
|
port: 3000,
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
dist
|
||||||
|
.wrangler
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
|
||||||
|
# Temp
|
||||||
|
gitignore
|
||||||
|
|
||||||
|
# System Files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# SolidStart
|
||||||
|
|
||||||
|
Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com);
|
||||||
|
|
||||||
|
## Creating a project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# create a new project in the current directory
|
||||||
|
npm init solid@latest
|
||||||
|
|
||||||
|
# create a new project in my-app
|
||||||
|
npm init solid@latest my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# or start the server and open the app in a new browser tab
|
||||||
|
npm run dev -- --open
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Solid apps are built with _presets_, which optimise your project for deployment to different environments.
|
||||||
|
|
||||||
|
By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add it to the `devDependencies` in `package.json` and specify in your `app.config.js`.
|
||||||
|
|
||||||
|
## This project was created with the [Solid CLI](https://github.com/solidjs-community/solid-cli)
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "@opencode-ai/enterprise",
|
||||||
|
"version": "1.0.109",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"typecheck": "tsgo --noEmit",
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"build:cloudflare": "OPENCODE_DEPLOYMENT_TARGET=cloudflare vite build",
|
||||||
|
"start": "vite start"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@opencode-ai/util": "workspace:*",
|
||||||
|
"@opencode-ai/ui": "workspace:*",
|
||||||
|
"@solidjs/router": "catalog:",
|
||||||
|
"@solidjs/start": "catalog:",
|
||||||
|
"@solidjs/meta": "catalog:",
|
||||||
|
"hono": "catalog:",
|
||||||
|
"hono-openapi": "catalog:",
|
||||||
|
"luxon": "catalog:",
|
||||||
|
"nitro": "3.0.1-alpha.1",
|
||||||
|
"solid-js": "catalog:",
|
||||||
|
"zod": "catalog:"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@cloudflare/workers-types": "catalog:",
|
||||||
|
"@tailwindcss/vite": "catalog:",
|
||||||
|
"@typescript/native-preview": "catalog:",
|
||||||
|
"@types/luxon": "catalog:",
|
||||||
|
"tailwindcss": "catalog:",
|
||||||
|
"typescript": "catalog:",
|
||||||
|
"vite": "catalog:"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22"
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
../../ui/src/assets/favicon/favicon.svg
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import "@opencode-ai/ui/styles/tailwind";
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Router } from "@solidjs/router"
|
||||||
|
import { FileRoutes } from "@solidjs/start/router"
|
||||||
|
import { Fonts } from "@opencode-ai/ui/fonts"
|
||||||
|
import { MetaProvider } from "@solidjs/meta"
|
||||||
|
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
|
||||||
|
import "./app.css"
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<Router
|
||||||
|
root={(props) => (
|
||||||
|
<MarkedProvider>
|
||||||
|
<MetaProvider>
|
||||||
|
<Fonts />
|
||||||
|
{props.children}
|
||||||
|
</MetaProvider>
|
||||||
|
</MarkedProvider>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FileRoutes />
|
||||||
|
</Router>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import { FileDiff, Message, Model, Part, Session, SessionStatus } from "@opencode-ai/sdk"
|
||||||
|
import { fn } from "@opencode-ai/util/fn"
|
||||||
|
import { iife } from "@opencode-ai/util/iife"
|
||||||
|
import z from "zod"
|
||||||
|
import { Storage } from "./storage"
|
||||||
|
|
||||||
|
export namespace Share {
|
||||||
|
export const Info = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
secret: z.string(),
|
||||||
|
})
|
||||||
|
export type Info = z.infer<typeof Info>
|
||||||
|
|
||||||
|
export const Data = z.discriminatedUnion("type", [
|
||||||
|
z.object({
|
||||||
|
type: z.literal("session"),
|
||||||
|
data: z.custom<Session>(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal("message"),
|
||||||
|
data: z.custom<Message>(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal("part"),
|
||||||
|
data: z.custom<Part>(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal("session_diff"),
|
||||||
|
data: z.custom<FileDiff[]>(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal("session_status"),
|
||||||
|
data: z.custom<SessionStatus>(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal("model"),
|
||||||
|
data: z.custom<Model[]>(),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
export type Data = z.infer<typeof Data>
|
||||||
|
|
||||||
|
export const create = fn(Info.pick({ id: true }), async (body) => {
|
||||||
|
const info: Info = {
|
||||||
|
id: body.id,
|
||||||
|
secret: crypto.randomUUID(),
|
||||||
|
}
|
||||||
|
const exists = await get(info.id)
|
||||||
|
if (exists) throw new Errors.AlreadyExists(info.id)
|
||||||
|
await Storage.write(["share", info.id], info)
|
||||||
|
console.log("created share", info.id)
|
||||||
|
return info
|
||||||
|
})
|
||||||
|
|
||||||
|
async function get(sessionID: string) {
|
||||||
|
return Storage.read<Info>(["share", sessionID])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const remove = fn(Info.pick({ id: true, secret: true }), async (body) => {
|
||||||
|
const share = await get(body.id)
|
||||||
|
if (!share) throw new Errors.NotFound(body.id)
|
||||||
|
if (share.secret !== body.secret) throw new Errors.InvalidSecret(body.id)
|
||||||
|
await Storage.remove(["share", body.id])
|
||||||
|
const list = await Storage.list(["share_data", body.id])
|
||||||
|
for (const item of list) {
|
||||||
|
await Storage.remove(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function data(sessionID: string) {
|
||||||
|
const list = await Storage.list(["share_data", sessionID])
|
||||||
|
const promises = []
|
||||||
|
for (const item of list) {
|
||||||
|
promises.push(
|
||||||
|
iife(async () => {
|
||||||
|
const [, , type] = item
|
||||||
|
return {
|
||||||
|
type: type as any,
|
||||||
|
data: await Storage.read<any>(item),
|
||||||
|
} as Data
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return await Promise.all(promises)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sync = fn(
|
||||||
|
z.object({
|
||||||
|
share: Info,
|
||||||
|
data: Data.array(),
|
||||||
|
}),
|
||||||
|
async (input) => {
|
||||||
|
const share = await get(input.share.id)
|
||||||
|
if (!share) throw new Errors.NotFound(input.share.id)
|
||||||
|
if (share.secret !== input.share.secret) throw new Errors.InvalidSecret(input.share.id)
|
||||||
|
const promises = []
|
||||||
|
for (const item of input.data) {
|
||||||
|
promises.push(
|
||||||
|
iife(async () => {
|
||||||
|
switch (item.type) {
|
||||||
|
case "session":
|
||||||
|
await Storage.write(["share_data", input.share.id, "session"], item.data)
|
||||||
|
break
|
||||||
|
case "message":
|
||||||
|
await Storage.write(["share_data", input.share.id, "message", item.data.id], item.data)
|
||||||
|
break
|
||||||
|
case "part":
|
||||||
|
await Storage.write(
|
||||||
|
["share_data", input.share.id, "part", item.data.messageID, item.data.id],
|
||||||
|
item.data,
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case "session_diff":
|
||||||
|
await Storage.write(["share_data", input.share.id, "session_diff"], item.data)
|
||||||
|
break
|
||||||
|
case "session_status":
|
||||||
|
await Storage.write(["share_data", input.share.id, "session_status"], item.data)
|
||||||
|
break
|
||||||
|
case "model":
|
||||||
|
await Storage.write(["share_data", input.share.id, "model"], item.data)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
await Promise.all(promises)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const Errors = {
|
||||||
|
NotFound: class extends Error {
|
||||||
|
constructor(public id: string) {
|
||||||
|
super(`Share not found: ${id}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
InvalidSecret: class extends Error {
|
||||||
|
constructor(public id: string) {
|
||||||
|
super(`Share secret invalid: ${id}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
AlreadyExists: class extends Error {
|
||||||
|
constructor(public id: string) {
|
||||||
|
super(`Share already exists: ${id}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import {
|
||||||
|
S3Client,
|
||||||
|
PutObjectCommand,
|
||||||
|
GetObjectCommand,
|
||||||
|
DeleteObjectCommand,
|
||||||
|
ListObjectsV2Command,
|
||||||
|
} from "@aws-sdk/client-s3"
|
||||||
|
import { lazy } from "@opencode-ai/util/lazy"
|
||||||
|
|
||||||
|
export namespace Storage {
|
||||||
|
export interface Adapter {
|
||||||
|
read(path: string): Promise<string | undefined>
|
||||||
|
write(path: string, value: string): Promise<void>
|
||||||
|
remove(path: string): Promise<void>
|
||||||
|
list(prefix: string): Promise<string[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAdapter(client: S3Client, bucket: string): Adapter {
|
||||||
|
return {
|
||||||
|
async read(path: string): Promise<string | undefined> {
|
||||||
|
try {
|
||||||
|
console.log("reading", bucket, path)
|
||||||
|
const command = new GetObjectCommand({
|
||||||
|
Bucket: bucket,
|
||||||
|
Key: path,
|
||||||
|
})
|
||||||
|
const response = await client.send(command)
|
||||||
|
if (!response.Body) return undefined
|
||||||
|
return response.Body.transformToString()
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.name === "NoSuchKey") return undefined
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async write(path: string, value: string): Promise<void> {
|
||||||
|
const command = new PutObjectCommand({
|
||||||
|
Bucket: bucket,
|
||||||
|
Key: path,
|
||||||
|
Body: value,
|
||||||
|
ContentType: "application/json",
|
||||||
|
})
|
||||||
|
await client.send(command)
|
||||||
|
},
|
||||||
|
|
||||||
|
async remove(path: string): Promise<void> {
|
||||||
|
const command = new DeleteObjectCommand({
|
||||||
|
Bucket: bucket,
|
||||||
|
Key: path,
|
||||||
|
})
|
||||||
|
await client.send(command)
|
||||||
|
},
|
||||||
|
|
||||||
|
async list(prefix: string): Promise<string[]> {
|
||||||
|
const command = new ListObjectsV2Command({
|
||||||
|
Bucket: bucket,
|
||||||
|
Prefix: prefix,
|
||||||
|
})
|
||||||
|
const response = await client.send(command)
|
||||||
|
return response.Contents?.map((c) => c.Key!) || []
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function s3(): Adapter {
|
||||||
|
const bucket = process.env.OPENCODE_STORAGE_BUCKET!
|
||||||
|
const client = new S3Client({
|
||||||
|
region: process.env.OPENCODE_STORAGE_REGION,
|
||||||
|
credentials: process.env.OPENCODE_STORAGE_ACCESS_KEY_ID
|
||||||
|
? {
|
||||||
|
accessKeyId: process.env.OPENCODE_STORAGE_ACCESS_KEY_ID!,
|
||||||
|
secretAccessKey: process.env.OPENCODE_STORAGE_SECRET_ACCESS_KEY!,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
})
|
||||||
|
return createAdapter(client, bucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
function r2() {
|
||||||
|
const accountId = process.env.OPENCODE_STORAGE_ACCOUNT_ID!
|
||||||
|
const accessKeyId = process.env.OPENCODE_STORAGE_ACCESS_KEY_ID!
|
||||||
|
const secretAccessKey = process.env.OPENCODE_STORAGE_SECRET_ACCESS_KEY!
|
||||||
|
const bucket = process.env.OPENCODE_STORAGE_BUCKET!
|
||||||
|
|
||||||
|
const client = new S3Client({
|
||||||
|
region: "auto",
|
||||||
|
endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId,
|
||||||
|
secretAccessKey,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return createAdapter(client, bucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
const adapter = lazy(() => {
|
||||||
|
const type = process.env.OPENCODE_STORAGE_ADAPTER
|
||||||
|
if (type === "r2") return r2()
|
||||||
|
if (type === "s3") return s3()
|
||||||
|
throw new Error("No storage adapter configured")
|
||||||
|
})
|
||||||
|
|
||||||
|
function resolve(key: string[]) {
|
||||||
|
return key.join("/") + ".json"
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function read<T>(key: string[]) {
|
||||||
|
const result = await adapter().read(resolve(key))
|
||||||
|
if (!result) return undefined
|
||||||
|
return JSON.parse(result) as T
|
||||||
|
}
|
||||||
|
|
||||||
|
export function write<T>(key: string[], value: T) {
|
||||||
|
return adapter().write(resolve(key), JSON.stringify(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function remove(key: string[]) {
|
||||||
|
return adapter().remove(resolve(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function list(prefix: string[]) {
|
||||||
|
const p = prefix.join("/") + (prefix.length ? "/" : "")
|
||||||
|
const result = await adapter().list(p)
|
||||||
|
return result.map((x) => x.replace(/\.json$/, "").split("/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function update<T>(key: string[], fn: (draft: T) => void) {
|
||||||
|
const val = await read<T>(key)
|
||||||
|
if (!val) throw new Error("Not found")
|
||||||
|
fn(val)
|
||||||
|
await write(key, val)
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// @refresh reload
|
||||||
|
import { mount, StartClient } from "@solidjs/start/client"
|
||||||
|
|
||||||
|
mount(() => <StartClient />, document.getElementById("app")!)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// @refresh reload
|
||||||
|
import { createHandler, StartServer } from "@solidjs/start/server"
|
||||||
|
|
||||||
|
export default createHandler(() => (
|
||||||
|
<StartServer
|
||||||
|
document={({ assets, children, scripts }) => (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="shortcut icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<title>OpenCode</title>
|
||||||
|
{assets}
|
||||||
|
</head>
|
||||||
|
<body class="antialiased overscroll-none select-none text-12-regular">
|
||||||
|
<div id="app">{children}</div>
|
||||||
|
{scripts}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="@solidjs/start/env" />
|
||||||
|
|
||||||
|
export declare module "@solidjs/start/server" {
|
||||||
|
export type APIEvent = { request: Request }
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { A } from "@solidjs/router"
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
return (
|
||||||
|
<main class="text-center mx-auto text-gray-700 p-4">
|
||||||
|
<h1 class="max-6-xs text-6xl text-sky-700 font-thin uppercase my-16">Not Found</h1>
|
||||||
|
<p class="mt-8">
|
||||||
|
Visit{" "}
|
||||||
|
<a href="https://solidjs.com" target="_blank" class="text-sky-600 hover:underline">
|
||||||
|
solidjs.com
|
||||||
|
</a>{" "}
|
||||||
|
to learn how to build Solid apps.
|
||||||
|
</p>
|
||||||
|
<p class="my-4">
|
||||||
|
<A href="/" class="text-sky-600 hover:underline">
|
||||||
|
Home
|
||||||
|
</A>
|
||||||
|
{" - "}
|
||||||
|
<A href="/about" class="text-sky-600 hover:underline">
|
||||||
|
About Page
|
||||||
|
</A>
|
||||||
|
</p>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import type { APIEvent } from "@solidjs/start/server"
|
||||||
|
import { Hono } from "hono"
|
||||||
|
import { describeRoute, openAPIRouteHandler, resolver } from "hono-openapi"
|
||||||
|
import { validator } from "hono-openapi"
|
||||||
|
import z from "zod"
|
||||||
|
import { cors } from "hono/cors"
|
||||||
|
import { Share } from "~/core/share"
|
||||||
|
|
||||||
|
const app = new Hono()
|
||||||
|
|
||||||
|
app
|
||||||
|
.basePath("/api")
|
||||||
|
.use(cors())
|
||||||
|
.get(
|
||||||
|
"/doc",
|
||||||
|
openAPIRouteHandler(app, {
|
||||||
|
documentation: {
|
||||||
|
info: {
|
||||||
|
title: "Opencode Enterprise API",
|
||||||
|
version: "1.0.0",
|
||||||
|
description: "Opencode Enterprise API endpoints",
|
||||||
|
},
|
||||||
|
openapi: "3.1.1",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.post(
|
||||||
|
"/share",
|
||||||
|
describeRoute({
|
||||||
|
description: "Create a share",
|
||||||
|
operationId: "share.create",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Success",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
url: z.string(),
|
||||||
|
secret: z.string(),
|
||||||
|
})
|
||||||
|
.meta({ ref: "Share" }),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
validator("json", z.object({ sessionID: z.string() })),
|
||||||
|
async (c) => {
|
||||||
|
const body = c.req.valid("json")
|
||||||
|
const share = await Share.create({ id: body.sessionID })
|
||||||
|
const protocol = c.req.header("x-forwarded-proto") ?? c.req.header("x-forwarded-protocol") ?? "https"
|
||||||
|
const host = c.req.header("x-forwarded-host") ?? c.req.header("host")
|
||||||
|
return c.json({
|
||||||
|
secret: share.secret,
|
||||||
|
url: `${protocol}://${host}/share/${share.id}`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.post(
|
||||||
|
"/share/:sessionID/sync",
|
||||||
|
describeRoute({
|
||||||
|
description: "Sync share data",
|
||||||
|
operationId: "share.sync",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Success",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(z.object({})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
validator("param", z.object({ sessionID: z.string() })),
|
||||||
|
validator("json", z.object({ secret: z.string(), data: Share.Data.array() })),
|
||||||
|
async (c) => {
|
||||||
|
const { sessionID } = c.req.valid("param")
|
||||||
|
const body = c.req.valid("json")
|
||||||
|
await Share.sync({
|
||||||
|
share: { id: sessionID, secret: body.secret },
|
||||||
|
data: body.data,
|
||||||
|
})
|
||||||
|
return c.json({})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.get(
|
||||||
|
"/share/:sessionID/data",
|
||||||
|
describeRoute({
|
||||||
|
description: "Get share data",
|
||||||
|
operationId: "share.data",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Success",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(z.array(Share.Data)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
validator("param", z.object({ sessionID: z.string() })),
|
||||||
|
async (c) => {
|
||||||
|
const { sessionID } = c.req.valid("param")
|
||||||
|
return c.json(await Share.data(sessionID))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.delete(
|
||||||
|
"/share/:sessionID",
|
||||||
|
describeRoute({
|
||||||
|
description: "Remove a share",
|
||||||
|
operationId: "share.remove",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Success",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(z.object({})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
validator("param", z.object({ sessionID: z.string() })),
|
||||||
|
validator("json", z.object({ secret: z.string() })),
|
||||||
|
async (c) => {
|
||||||
|
const { sessionID } = c.req.valid("param")
|
||||||
|
const body = c.req.valid("json")
|
||||||
|
await Share.remove({ id: sessionID, secret: body.secret })
|
||||||
|
return c.json({})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export function GET(event: APIEvent) {
|
||||||
|
return app.fetch(event.request)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function POST(event: APIEvent) {
|
||||||
|
return app.fetch(event.request)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PUT(event: APIEvent) {
|
||||||
|
return app.fetch(event.request)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(event: APIEvent) {
|
||||||
|
return app.fetch(event.request)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { ParentProps } from "solid-js"
|
||||||
|
|
||||||
|
export default function Share(props: ParentProps) {
|
||||||
|
return props.children
|
||||||
|
}
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
import { FileDiff, Message, Model, Part, Session, SessionStatus, UserMessage } from "@opencode-ai/sdk"
|
||||||
|
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
||||||
|
import { SessionReview } from "@opencode-ai/ui/session-review"
|
||||||
|
import { DataProvider } from "@opencode-ai/ui/context"
|
||||||
|
import { createAsync, query, RouteDefinition, useParams } from "@solidjs/router"
|
||||||
|
import { createMemo, ErrorBoundary, For, Match, Show, Switch } from "solid-js"
|
||||||
|
import { Share } from "~/core/share"
|
||||||
|
import { Logo, Mark } from "@opencode-ai/ui/logo"
|
||||||
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
|
import { iife } from "@opencode-ai/util/iife"
|
||||||
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
|
import { NamedError } from "@opencode-ai/util/error"
|
||||||
|
import { DateTime } from "luxon"
|
||||||
|
import { MessageNav } from "@opencode-ai/ui/message-nav"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import z from "zod"
|
||||||
|
import NotFound from "../[...404]"
|
||||||
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
|
|
||||||
|
const SessionDataMissingError = NamedError.create(
|
||||||
|
"SessionDataMissingError",
|
||||||
|
z.object({
|
||||||
|
sessionID: z.string(),
|
||||||
|
message: z.string().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const getData = query(async (sessionID) => {
|
||||||
|
const data = await Share.data(sessionID)
|
||||||
|
const result: {
|
||||||
|
session: Session[]
|
||||||
|
session_diff: {
|
||||||
|
[sessionID: string]: FileDiff[]
|
||||||
|
}
|
||||||
|
session_status: {
|
||||||
|
[sessionID: string]: SessionStatus
|
||||||
|
}
|
||||||
|
message: {
|
||||||
|
[sessionID: string]: Message[]
|
||||||
|
}
|
||||||
|
part: {
|
||||||
|
[messageID: string]: Part[]
|
||||||
|
}
|
||||||
|
model: {
|
||||||
|
[sessionID: string]: Model[]
|
||||||
|
}
|
||||||
|
} = {
|
||||||
|
session: [],
|
||||||
|
session_diff: {
|
||||||
|
[sessionID]: [],
|
||||||
|
},
|
||||||
|
session_status: {
|
||||||
|
[sessionID]: {
|
||||||
|
type: "idle",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
message: {},
|
||||||
|
part: {},
|
||||||
|
model: {},
|
||||||
|
}
|
||||||
|
for (const item of data) {
|
||||||
|
switch (item.type) {
|
||||||
|
case "session":
|
||||||
|
result.session.push(item.data)
|
||||||
|
break
|
||||||
|
case "session_diff":
|
||||||
|
result.session_diff[sessionID] = item.data
|
||||||
|
break
|
||||||
|
case "session_status":
|
||||||
|
result.session_status[sessionID] = item.data
|
||||||
|
break
|
||||||
|
case "message":
|
||||||
|
result.message[item.data.sessionID] = result.message[item.data.sessionID] ?? []
|
||||||
|
result.message[item.data.sessionID].push(item.data)
|
||||||
|
break
|
||||||
|
case "part":
|
||||||
|
result.part[item.data.messageID] = result.part[item.data.messageID] ?? []
|
||||||
|
result.part[item.data.messageID].push(item.data)
|
||||||
|
break
|
||||||
|
case "model":
|
||||||
|
result.model[sessionID] = item.data
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const match = Binary.search(result.session, sessionID!, (s) => s.id)
|
||||||
|
if (!match.found) throw new SessionDataMissingError({ sessionID })
|
||||||
|
return result
|
||||||
|
}, "getShareData")
|
||||||
|
|
||||||
|
export const route = {
|
||||||
|
preload: ({ params }) => getData(params.sessionID),
|
||||||
|
} satisfies RouteDefinition
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const params = useParams()
|
||||||
|
const data = createAsync(async () => {
|
||||||
|
if (!params.sessionID) throw new Error("Missing sessionID")
|
||||||
|
return getData(params.sessionID)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary
|
||||||
|
fallback={(e) => {
|
||||||
|
return (
|
||||||
|
<Show when={e.message === "SessionDataMissingError"}>
|
||||||
|
<NotFound />
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show when={data()}>
|
||||||
|
{(data) => (
|
||||||
|
<DataProvider data={data()}>
|
||||||
|
{iife(() => {
|
||||||
|
const [store, setStore] = createStore({
|
||||||
|
messageId: undefined as string | undefined,
|
||||||
|
})
|
||||||
|
const match = createMemo(() => Binary.search(data().session, params.sessionID!, (s) => s.id))
|
||||||
|
if (!match().found) throw new Error(`Session ${params.sessionID} not found`)
|
||||||
|
const info = createMemo(() => data().session[match().index])
|
||||||
|
const messages = createMemo(() =>
|
||||||
|
params.sessionID
|
||||||
|
? (data().message[params.sessionID]?.filter((m) => m.role === "user") ?? []).sort(
|
||||||
|
(a, b) => b.time.created - a.time.created,
|
||||||
|
)
|
||||||
|
: [],
|
||||||
|
)
|
||||||
|
const firstUserMessage = createMemo(() => messages().at(0))
|
||||||
|
const activeMessage = createMemo(
|
||||||
|
() => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(),
|
||||||
|
)
|
||||||
|
function setActiveMessage(message: UserMessage | undefined) {
|
||||||
|
if (message) {
|
||||||
|
setStore("messageId", message.id)
|
||||||
|
} else {
|
||||||
|
setStore("messageId", undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const provider = createMemo(() => activeMessage()?.model?.providerID)
|
||||||
|
const modelID = createMemo(() => activeMessage()?.model?.modelID)
|
||||||
|
const model = createMemo(() => data().model[params.sessionID!]?.find((m) => m.id === modelID()))
|
||||||
|
const diffs = createMemo(() => data().session_diff[params.sessionID!] ?? [])
|
||||||
|
|
||||||
|
const title = () => (
|
||||||
|
<div class="flex flex-col gap-4 shrink-0">
|
||||||
|
<div class="h-8 flex gap-4 items-center justify-start self-stretch">
|
||||||
|
<div class="pl-[2.5px] pr-2 flex items-center gap-1.75 bg-surface-strong shadow-xs-border-base">
|
||||||
|
<Mark class="shrink-0 w-3 my-0.5" />
|
||||||
|
<div class="text-12-mono text-text-base">v{info().version}</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<img src={`https://models.dev/logos/${provider()}.svg`} class="size-3.5 shrink-0 dark:invert" />
|
||||||
|
<div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-12-regular text-text-weaker">
|
||||||
|
{DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-left text-16-medium text-text-strong">{info().title}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const turns = () => (
|
||||||
|
<div class="relative mt-2 pt-6 pb-8 px-4 min-w-0 w-full h-full overflow-y-auto no-scrollbar">
|
||||||
|
{title()}
|
||||||
|
<div class="flex flex-col gap-15 items-start justify-start mt-4">
|
||||||
|
<For each={messages()}>
|
||||||
|
{(message) => (
|
||||||
|
<SessionTurn
|
||||||
|
sessionID={params.sessionID!}
|
||||||
|
messageID={message.id}
|
||||||
|
classes={{
|
||||||
|
root: "min-w-0 w-full relative",
|
||||||
|
content:
|
||||||
|
"flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-center pt-20 pb-8 shrink-0">
|
||||||
|
<Logo class="w-58.5 opacity-12" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="relative bg-background-stronger w-screen h-screen overflow-hidden flex flex-col">
|
||||||
|
<header class="h-12 px-6 py-2 flex items-center justify-between self-stretch bg-background-base border-b border-border-weak-base">
|
||||||
|
<div class="">
|
||||||
|
<a href="https://opencode.ai">
|
||||||
|
<Mark />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3 items-center">
|
||||||
|
<IconButton
|
||||||
|
as={"a"}
|
||||||
|
href="https://github.com/sst/opencode"
|
||||||
|
target="_blank"
|
||||||
|
icon="github"
|
||||||
|
variant="ghost"
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
as={"a"}
|
||||||
|
href="https://opencode.ai/discord"
|
||||||
|
target="_blank"
|
||||||
|
icon="discord"
|
||||||
|
variant="ghost"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="select-text flex flex-col flex-1 min-h-0">
|
||||||
|
<div class="hidden md:flex w-full flex-1 min-h-0">
|
||||||
|
<div
|
||||||
|
classList={{
|
||||||
|
"@container relative shrink-0 pt-14 flex flex-col gap-10 min-h-0 w-full mx-auto": true,
|
||||||
|
"px-21 @4xl:px-6 max-w-2xl": diffs().length > 0,
|
||||||
|
"px-6 max-w-2xl": diffs().length === 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title()}
|
||||||
|
<div class="flex items-start justify-start h-full min-h-0">
|
||||||
|
<Show when={messages().length > 1}>
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
classList={{
|
||||||
|
"xl:hidden": true,
|
||||||
|
"absolute right-[90%]": diffs().length > 0,
|
||||||
|
"absolute right-full": diffs().length === 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MessageNav
|
||||||
|
classList={{
|
||||||
|
"mt-0.5 mr-8": diffs().length === 0,
|
||||||
|
"mt-2.5 mr-3": diffs().length > 0,
|
||||||
|
}}
|
||||||
|
messages={messages()}
|
||||||
|
current={activeMessage()}
|
||||||
|
onMessageSelect={setActiveMessage}
|
||||||
|
size={!diffs().length ? "normal" : "compact"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
classList={{
|
||||||
|
"hidden xl:block": true,
|
||||||
|
"absolute right-[90%]": diffs().length > 0,
|
||||||
|
"absolute right-full": diffs().length === 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MessageNav
|
||||||
|
classList={{
|
||||||
|
"mt-0.5 mr-8": diffs().length === 0,
|
||||||
|
"mt-2.5 mr-3": diffs().length > 0,
|
||||||
|
}}
|
||||||
|
messages={messages()}
|
||||||
|
current={activeMessage()}
|
||||||
|
onMessageSelect={setActiveMessage}
|
||||||
|
size={!diffs().length ? "normal" : "compact"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</Show>
|
||||||
|
<SessionTurn
|
||||||
|
sessionID={params.sessionID!}
|
||||||
|
messageID={store.messageId ?? firstUserMessage()!.id!}
|
||||||
|
classes={{ root: "grow", content: "flex flex-col justify-between", container: "pb-20" }}
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center pb-8 shrink-0">
|
||||||
|
<Logo class="w-58.5 opacity-12" />
|
||||||
|
</div>
|
||||||
|
</SessionTurn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Show when={diffs().length}>
|
||||||
|
<div class="relative grow px-6 pt-14 flex-1 min-h-0 border-l border-border-weak-base">
|
||||||
|
<SessionReview diffs={diffs()} class="pb-20" />
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<Switch>
|
||||||
|
<Match when={diffs().length}>
|
||||||
|
<Tabs class="md:hidden">
|
||||||
|
<Tabs.List>
|
||||||
|
<Tabs.Trigger value="session" class="w-1/2" classes={{ button: "w-full" }}>
|
||||||
|
Session
|
||||||
|
</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="review" class="w-1/2" classes={{ button: "w-full" }}>
|
||||||
|
5 Files Changed
|
||||||
|
</Tabs.Trigger>
|
||||||
|
</Tabs.List>
|
||||||
|
<Tabs.Content value="session" class="!overflow-hidden">
|
||||||
|
{turns()}
|
||||||
|
</Tabs.Content>
|
||||||
|
<Tabs.Content value="review" class="!overflow-hidden">
|
||||||
|
<div class="relative px-4 pt-8 h-full overflow-y-auto no-scrollbar">
|
||||||
|
<SessionReview diffs={diffs()} class="pb-20" />
|
||||||
|
</div>
|
||||||
|
</Tabs.Content>
|
||||||
|
</Tabs>
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<div class="md:hidden !overflow-hidden">{turns()}</div>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</DataProvider>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</ErrorBoundary>
|
||||||
|
)
|
||||||
|
}
|
||||||
Vendored
+131
@@ -0,0 +1,131 @@
|
|||||||
|
/* This file is auto-generated by SST. Do not edit. */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/* deno-fmt-ignore-file */
|
||||||
|
|
||||||
|
import "sst"
|
||||||
|
declare module "sst" {
|
||||||
|
export interface Resource {
|
||||||
|
ADMIN_SECRET: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
AUTH_API_URL: {
|
||||||
|
type: "sst.sst.Linkable"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
AWS_SES_ACCESS_KEY_ID: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
AWS_SES_SECRET_ACCESS_KEY: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
CLOUDFLARE_API_TOKEN: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
CLOUDFLARE_DEFAULT_ACCOUNT_ID: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
Console: {
|
||||||
|
type: "sst.cloudflare.SolidStart"
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
Database: {
|
||||||
|
database: string
|
||||||
|
host: string
|
||||||
|
password: string
|
||||||
|
port: number
|
||||||
|
type: "sst.sst.Linkable"
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
Desktop: {
|
||||||
|
type: "sst.cloudflare.StaticSite"
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
EMAILOCTOPUS_API_KEY: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
GITHUB_APP_ID: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
GITHUB_APP_PRIVATE_KEY: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
GITHUB_CLIENT_ID_CONSOLE: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
GITHUB_CLIENT_SECRET_CONSOLE: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
GOOGLE_CLIENT_ID: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
HONEYCOMB_API_KEY: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
R2AccessKey: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
R2SecretKey: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
STRIPE_SECRET_KEY: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
STRIPE_WEBHOOK_SECRET: {
|
||||||
|
type: "sst.sst.Linkable"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
Web: {
|
||||||
|
type: "sst.cloudflare.Astro"
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
ZEN_MODELS1: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
ZEN_MODELS2: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
ZEN_MODELS3: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
ZEN_MODELS4: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// cloudflare
|
||||||
|
import * as cloudflare from "@cloudflare/workers-types"
|
||||||
|
declare module "sst" {
|
||||||
|
export interface Resource {
|
||||||
|
Api: cloudflare.Service
|
||||||
|
AuthApi: cloudflare.Service
|
||||||
|
AuthStorage: cloudflare.KVNamespace
|
||||||
|
Bucket: cloudflare.R2Bucket
|
||||||
|
ConsoleData: cloudflare.R2Bucket
|
||||||
|
EnterpriseStorage: cloudflare.R2Bucket
|
||||||
|
GatewayKv: cloudflare.KVNamespace
|
||||||
|
LogProcessor: cloudflare.Service
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
import "sst"
|
||||||
|
export {}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "solid-js",
|
||||||
|
"allowJs": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"types": ["@cloudflare/workers-types", "vite/client"],
|
||||||
|
"isolatedModules": true,
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { defineConfig, PluginOption } from "vite"
|
||||||
|
import { solidStart } from "@solidjs/start/config"
|
||||||
|
import { nitro } from "nitro/vite"
|
||||||
|
import tailwindcss from "@tailwindcss/vite"
|
||||||
|
|
||||||
|
const nitroConfig: any = (() => {
|
||||||
|
const target = process.env.OPENCODE_DEPLOYMENT_TARGET
|
||||||
|
if (target === "cloudflare") {
|
||||||
|
return {
|
||||||
|
compatibilityDate: "2024-09-19",
|
||||||
|
preset: "cloudflare_module",
|
||||||
|
cloudflare: {
|
||||||
|
nodeCompat: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
})()
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [tailwindcss(), solidStart() as PluginOption, nitro(nitroConfig)],
|
||||||
|
server: {
|
||||||
|
host: "0.0.0.0",
|
||||||
|
allowedHosts: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
id = "opencode"
|
id = "opencode"
|
||||||
name = "OpenCode"
|
name = "OpenCode"
|
||||||
description = "The AI coding agent built for the terminal"
|
description = "The AI coding agent built for the terminal"
|
||||||
version = "1.0.85"
|
version = "1.0.109"
|
||||||
schema_version = 1
|
schema_version = 1
|
||||||
authors = ["Anomaly"]
|
authors = ["Anomaly"]
|
||||||
repository = "https://github.com/sst/opencode"
|
repository = "https://github.com/sst/opencode"
|
||||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
|||||||
icon = "./icons/opencode.svg"
|
icon = "./icons/opencode.svg"
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-aarch64]
|
[agent_servers.opencode.targets.darwin-aarch64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.85/opencode-darwin-arm64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.109/opencode-darwin-arm64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-x86_64]
|
[agent_servers.opencode.targets.darwin-x86_64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.85/opencode-darwin-x64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.109/opencode-darwin-x64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-aarch64]
|
[agent_servers.opencode.targets.linux-aarch64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.85/opencode-linux-arm64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.109/opencode-linux-arm64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-x86_64]
|
[agent_servers.opencode.targets.linux-x86_64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.85/opencode-linux-x64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.109/opencode-linux-x64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.windows-x86_64]
|
[agent_servers.opencode.targets.windows-x86_64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.85/opencode-windows-x64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.109/opencode-windows-x64.zip"
|
||||||
cmd = "./opencode.exe"
|
cmd = "./opencode.exe"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.0.85",
|
"version": "1.0.109",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
Vendored
+18
@@ -74,6 +74,14 @@ declare module "sst" {
|
|||||||
type: "sst.sst.Secret"
|
type: "sst.sst.Secret"
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
R2AccessKey: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
R2SecretKey: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
STRIPE_SECRET_KEY: {
|
STRIPE_SECRET_KEY: {
|
||||||
type: "sst.sst.Secret"
|
type: "sst.sst.Secret"
|
||||||
value: string
|
value: string
|
||||||
@@ -94,6 +102,14 @@ declare module "sst" {
|
|||||||
type: "sst.sst.Secret"
|
type: "sst.sst.Secret"
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
ZEN_MODELS3: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
ZEN_MODELS4: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// cloudflare
|
// cloudflare
|
||||||
@@ -104,6 +120,8 @@ declare module "sst" {
|
|||||||
AuthApi: cloudflare.Service
|
AuthApi: cloudflare.Service
|
||||||
AuthStorage: cloudflare.KVNamespace
|
AuthStorage: cloudflare.KVNamespace
|
||||||
Bucket: cloudflare.R2Bucket
|
Bucket: cloudflare.R2Bucket
|
||||||
|
ConsoleData: cloudflare.R2Bucket
|
||||||
|
EnterpriseStorage: cloudflare.R2Bucket
|
||||||
GatewayKv: cloudflare.KVNamespace
|
GatewayKv: cloudflare.KVNamespace
|
||||||
LogProcessor: cloudflare.Service
|
LogProcessor: cloudflare.Service
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
FROM alpine
|
||||||
|
|
||||||
|
# Disable the runtime transpiler cache by default inside Docker containers.
|
||||||
|
# On ephemeral containers, the cache is not useful
|
||||||
|
ARG BUN_RUNTIME_TRANSPILER_CACHE_PATH=0
|
||||||
|
ENV BUN_RUNTIME_TRANSPILER_CACHE_PATH=${BUN_RUNTIME_TRANSPILER_CACHE_PATH}
|
||||||
|
RUN apk add libgcc libstdc++
|
||||||
|
ADD ./dist/opencode-linux-x64-baseline-musl/bin/opencode /usr/local/bin/opencode
|
||||||
|
RUN opencode --version
|
||||||
|
ENTRYPOINT ["opencode"]
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.0.85",
|
"version": "1.0.109",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -18,8 +18,6 @@
|
|||||||
"./*": "./src/*.ts"
|
"./*": "./src/*.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ai-sdk/amazon-bedrock": "2.2.10",
|
|
||||||
"@ai-sdk/google-vertex": "3.0.16",
|
|
||||||
"@babel/core": "7.28.4",
|
"@babel/core": "7.28.4",
|
||||||
"@octokit/webhooks-types": "7.6.1",
|
"@octokit/webhooks-types": "7.6.1",
|
||||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||||
@@ -44,7 +42,14 @@
|
|||||||
"@actions/core": "1.11.1",
|
"@actions/core": "1.11.1",
|
||||||
"@actions/github": "6.0.1",
|
"@actions/github": "6.0.1",
|
||||||
"@agentclientprotocol/sdk": "0.5.1",
|
"@agentclientprotocol/sdk": "0.5.1",
|
||||||
|
"@ai-sdk/amazon-bedrock": "3.0.57",
|
||||||
|
"@ai-sdk/anthropic": "2.0.45",
|
||||||
|
"@ai-sdk/azure": "2.0.73",
|
||||||
|
"@ai-sdk/google": "2.0.42",
|
||||||
|
"@ai-sdk/google-vertex": "3.0.74",
|
||||||
"@ai-sdk/mcp": "0.0.8",
|
"@ai-sdk/mcp": "0.0.8",
|
||||||
|
"@ai-sdk/openai": "2.0.71",
|
||||||
|
"@ai-sdk/openai-compatible": "1.0.27",
|
||||||
"@clack/prompts": "1.0.0-alpha.1",
|
"@clack/prompts": "1.0.0-alpha.1",
|
||||||
"@hono/standard-validator": "0.1.5",
|
"@hono/standard-validator": "0.1.5",
|
||||||
"@hono/zod-validator": "catalog:",
|
"@hono/zod-validator": "catalog:",
|
||||||
@@ -55,8 +60,10 @@
|
|||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opentui/core": "0.1.47",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@opentui/solid": "0.1.47",
|
"@openrouter/ai-sdk-provider": "1.2.5",
|
||||||
|
"@opentui/core": "0.1.49",
|
||||||
|
"@opentui/solid": "0.1.49",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@pierre/precision-diffs": "catalog:",
|
"@pierre/precision-diffs": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
@@ -70,11 +77,12 @@
|
|||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"gray-matter": "4.0.3",
|
"gray-matter": "4.0.3",
|
||||||
"hono": "catalog:",
|
"hono": "catalog:",
|
||||||
"hono-openapi": "1.1.1",
|
"hono-openapi": "catalog:",
|
||||||
"ignore": "7.0.5",
|
"ignore": "7.0.5",
|
||||||
"jsonc-parser": "3.3.1",
|
"jsonc-parser": "3.3.1",
|
||||||
"minimatch": "10.0.3",
|
"minimatch": "10.0.3",
|
||||||
"open": "10.1.2",
|
"open": "10.1.2",
|
||||||
|
"opentui-spinner": "0.0.5",
|
||||||
"partial-json": "0.1.7",
|
"partial-json": "0.1.7",
|
||||||
"remeda": "catalog:",
|
"remeda": "catalog:",
|
||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
|
|||||||
@@ -108,9 +108,11 @@ for (const item of targets) {
|
|||||||
plugins: [solidPlugin],
|
plugins: [solidPlugin],
|
||||||
sourcemap: "external",
|
sourcemap: "external",
|
||||||
compile: {
|
compile: {
|
||||||
|
autoloadBunfig: false,
|
||||||
|
autoloadDotenv: false,
|
||||||
target: name.replace(pkg.name, "bun") as any,
|
target: name.replace(pkg.name, "bun") as any,
|
||||||
outfile: `dist/${name}/bin/opencode`,
|
outfile: `dist/${name}/bin/opencode`,
|
||||||
execArgv: [`--user-agent=opencode/${Script.version}`, `--env-file=""`, `--`],
|
execArgv: [`--user-agent=opencode/${Script.version}`, "--"],
|
||||||
windows: {},
|
windows: {},
|
||||||
},
|
},
|
||||||
entrypoints: ["./src/index.ts", parserWorker, workerPath],
|
entrypoints: ["./src/index.ts", parserWorker, workerPath],
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user