Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e15a39d90 | |||
| c70b975368 |
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
- name: Check for duplicate issues
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OPENCODE_PERMISSION: |
|
||||
{
|
||||
|
||||
@@ -21,8 +21,6 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Run opencode
|
||||
uses: sst/opencode/github@latest
|
||||
env:
|
||||
|
||||
@@ -28,7 +28,6 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.head_ref || github.ref_name }}
|
||||
|
||||
- name: Setup Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v20
|
||||
@@ -41,16 +40,12 @@ jobs:
|
||||
- name: Update flake.lock
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "📦 Updating flake.lock..."
|
||||
nix flake update
|
||||
echo "✅ flake.lock updated successfully"
|
||||
|
||||
- name: Update node_modules hash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "🔄 Updating node_modules hash..."
|
||||
nix/scripts/update-hashes.sh
|
||||
echo "✅ node_modules hash updated successfully"
|
||||
|
||||
- name: Commit hash changes
|
||||
env:
|
||||
@@ -58,8 +53,6 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "🔍 Checking for changes in tracked Nix files..."
|
||||
|
||||
summarize() {
|
||||
local status="$1"
|
||||
{
|
||||
@@ -77,24 +70,15 @@ jobs:
|
||||
FILES=(flake.lock flake.nix nix/node-modules.nix nix/hashes.json)
|
||||
STATUS="$(git status --short -- "${FILES[@]}" || true)"
|
||||
if [ -z "$STATUS" ]; then
|
||||
echo "✅ No changes detected. Hashes are already up to date."
|
||||
summarize "no changes"
|
||||
echo "No changes to tracked Nix files. Hashes are already up to date."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "📝 Changes detected:"
|
||||
echo "$STATUS"
|
||||
echo "🔗 Staging files..."
|
||||
git add "${FILES[@]}"
|
||||
echo "💾 Committing changes..."
|
||||
git commit -m "Update Nix flake.lock and hashes"
|
||||
echo "✅ Changes committed"
|
||||
|
||||
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
||||
echo "🌳 Pulling latest from branch: $BRANCH"
|
||||
git pull --rebase origin "$BRANCH"
|
||||
echo "🚀 Pushing changes to branch: $BRANCH"
|
||||
git push origin HEAD:"$BRANCH"
|
||||
echo "✅ Changes pushed successfully"
|
||||
|
||||
summarize "committed $(git rev-parse --short HEAD)"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
description: Git commit and push
|
||||
subtask: true
|
||||
---
|
||||
|
||||
commit and push
|
||||
|
||||
@@ -148,6 +148,3 @@
|
||||
| 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) |
|
||||
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1763934636,
|
||||
"narHash": "sha256-9glbI7f1uU+yzQCq5LwLgdZqx6svOhZWkd4JRY265fc=",
|
||||
"lastModified": 1763618868,
|
||||
"narHash": "sha256-v5afmLjn/uyD9EQuPBn7nZuaZVV9r+JerayK/4wvdWA=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "ee09932cedcef15aaf476f9343d1dea2cb77e261",
|
||||
"rev": "a8d610af3f1a5fb71e23e08434d8d61a466fc942",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -116,8 +116,6 @@ const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
|
||||
// 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_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
||||
|
||||
@@ -134,7 +132,6 @@ new sst.cloudflare.x.SolidStart("Console", {
|
||||
domain,
|
||||
path: "packages/console/app",
|
||||
link: [
|
||||
bucket,
|
||||
database,
|
||||
AUTH_API_URL,
|
||||
STRIPE_WEBHOOK_SECRET,
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
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,
|
||||
},
|
||||
})
|
||||
@@ -1,4 +0,0 @@
|
||||
export const SECRET = {
|
||||
R2AccessKey: new sst.Secret("R2AccessKey", "unknown"),
|
||||
R2SecretKey: new sst.Secret("R2SecretKey", "unknown"),
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/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,3 +1,3 @@
|
||||
{
|
||||
"nodeModules": "sha256-cieNNPXZd4Bg9bZtRq2H8L99e24U8p5d+d76SE7SeJc="
|
||||
"nodeModules": "sha256-N33FQyKF6IgGIRZ8NFd9o1/sjHMwbQ6KQcnMFyN0WmI="
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{ lib, stdenvNoCC, bun, fzf, ripgrep, makeBinaryWrapper }:
|
||||
{ lib, stdenv, stdenvNoCC, bun, fzf, ripgrep, makeBinaryWrapper }:
|
||||
args:
|
||||
let
|
||||
scripts = args.scripts;
|
||||
@@ -28,93 +28,66 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
||||
makeBinaryWrapper
|
||||
];
|
||||
|
||||
configurePhase = ''
|
||||
runHook preConfigure
|
||||
cp -R ${finalAttrs.node_modules}/. .
|
||||
runHook postConfigure
|
||||
'';
|
||||
|
||||
env.MODELS_DEV_API_JSON = args.modelsDev;
|
||||
env.OPENCODE_VERSION = args.version;
|
||||
env.OPENCODE_CHANNEL = "stable";
|
||||
dontConfigure = true;
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
|
||||
cp -r ${finalAttrs.node_modules}/node_modules .
|
||||
cp -r ${finalAttrs.node_modules}/packages .
|
||||
cp ${scripts + "/bun-build.ts"} bun-build.ts
|
||||
|
||||
(
|
||||
cd packages/opencode
|
||||
substituteInPlace bun-build.ts \
|
||||
--replace '@VERSION@' "${finalAttrs.version}"
|
||||
|
||||
chmod -R u+w ./node_modules
|
||||
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
|
||||
)
|
||||
export BUN_COMPILE_TARGET=${args.target}
|
||||
bun --bun bun-build.ts
|
||||
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
dontStrip = true;
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
cd packages/opencode
|
||||
if [ ! -d dist ]; then
|
||||
echo "ERROR: dist directory missing after bundle step"
|
||||
if [ ! -f opencode ]; then
|
||||
echo "ERROR: opencode binary not found in $(pwd)"
|
||||
ls -la
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f opencode-worker.js ]; then
|
||||
echo "ERROR: opencode worker bundle not found in $(pwd)"
|
||||
ls -la
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p $out/lib/opencode
|
||||
cp -r dist $out/lib/opencode/
|
||||
chmod -R u+w $out/lib/opencode/dist
|
||||
|
||||
# Select bundled worker assets deterministically (sorted find output)
|
||||
worker_file=$(find "$out/lib/opencode/dist" -type f \( -path '*/tui/worker.*' -o -name 'worker.*' \) | sort | head -n1)
|
||||
parser_worker_file=$(find "$out/lib/opencode/dist" -type f -name 'parser.worker.*' | sort | head -n1)
|
||||
if [ -z "$worker_file" ]; then
|
||||
echo "ERROR: bundled worker not found"
|
||||
exit 1
|
||||
install -Dm755 opencode $out/bin/opencode
|
||||
install -Dm644 opencode-worker.js $out/bin/opencode-worker.js
|
||||
if [ -f opencode-assets.manifest ]; then
|
||||
while IFS= read -r asset; do
|
||||
[ -z "$asset" ] && continue
|
||||
if [ ! -f "$asset" ]; then
|
||||
echo "ERROR: referenced asset \"$asset\" missing"
|
||||
exit 1
|
||||
fi
|
||||
install -Dm644 "$asset" "$out/bin/$(basename "$asset")"
|
||||
done < opencode-assets.manifest
|
||||
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
|
||||
'';
|
||||
|
||||
postInstall = ''
|
||||
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
|
||||
postFixup = ''
|
||||
wrapProgram "$out/bin/opencode" --prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]}
|
||||
'';
|
||||
|
||||
dontFixup = true;
|
||||
|
||||
meta = {
|
||||
description = "AI coding agent built for the terminal";
|
||||
longDescription = ''
|
||||
|
||||
@@ -24,13 +24,15 @@ for (const entry of directories) {
|
||||
if (!info.isDirectory()) {
|
||||
continue
|
||||
}
|
||||
const parsed = parseEntry(entry)
|
||||
if (!parsed) {
|
||||
const marker = entry.lastIndexOf("@")
|
||||
if (marker <= 0) {
|
||||
continue
|
||||
}
|
||||
const list = versions.get(parsed.name) ?? []
|
||||
list.push({ dir: full, version: parsed.version, label: entry })
|
||||
versions.set(parsed.name, list)
|
||||
const slug = entry.slice(0, marker).replace(/\+/g, "/")
|
||||
const version = entry.slice(marker + 1)
|
||||
const list = versions.get(slug) ?? []
|
||||
list.push({ dir: full, version, label: entry })
|
||||
versions.set(slug, list)
|
||||
}
|
||||
|
||||
const semverModule = (await import(join(bunRoot, "node_modules/semver"))) as
|
||||
@@ -77,12 +79,6 @@ for (const [slug, entry] of Array.from(selections.entries()).sort((a, b) => a[0]
|
||||
await mkdir(parent, { recursive: true })
|
||||
const linkPath = join(parent, leaf)
|
||||
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 resolved = relativeTarget.length === 0 ? "." : relativeTarget
|
||||
await rm(linkPath, { recursive: true, force: true })
|
||||
@@ -98,16 +94,3 @@ for (const line of rewrites.slice(0, 20)) {
|
||||
if (rewrites.length > 20) {
|
||||
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 }
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/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)
|
||||
@@ -30,7 +30,7 @@
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@pierre/precision-diffs": "0.5.5",
|
||||
"@pierre/precision-diffs": "0.5.4",
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"diff": "8.0.2",
|
||||
"ai": "5.0.97",
|
||||
@@ -48,7 +48,7 @@
|
||||
"vite": "7.1.4",
|
||||
"@solidjs/meta": "0.29.4",
|
||||
"@solidjs/router": "0.15.4",
|
||||
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
|
||||
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dbff19d",
|
||||
"solid-js": "1.9.10",
|
||||
"vite-plugin-solid": "2.11.10"
|
||||
}
|
||||
@@ -82,6 +82,9 @@
|
||||
"tree-sitter-bash",
|
||||
"web-tree-sitter"
|
||||
],
|
||||
"patchedDependencies": {
|
||||
"@solidjs/start@1.1.7": "patches/@solidjs%2Fstart@1.1.7.patch"
|
||||
},
|
||||
"overrides": {
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.112",
|
||||
"version": "1.0.101",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
@@ -18,7 +18,6 @@
|
||||
"@opencode-ai/console-core": "workspace:*",
|
||||
"@opencode-ai/console-mail": "workspace:*",
|
||||
"@opencode-ai/console-resource": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@solidjs/router": "catalog:",
|
||||
"@solidjs/start": "catalog:",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../../ui/src/assets/favicon/apple-touch-icon.png
|
||||
@@ -1 +0,0 @@
|
||||
../../../ui/src/assets/favicon/favicon-96x96.png
|
||||
@@ -0,0 +1,23 @@
|
||||
<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="#FDFCFC"/>
|
||||
<path d="M96 122.001V70.001H148V122.001H96Z" fill="#17181C"/>
|
||||
<path d="M148.004 122.001V70.001H200.004V122.001H148.004Z" fill="#17181C"/>
|
||||
<path d="M200.008 122.001V70.001H252.008V122.001H200.008Z" fill="#17181C"/>
|
||||
<path d="M251.996 122.001V70.001H303.996V122.001H251.996Z" fill="#17181C"/>
|
||||
<path d="M251.996 173.988V121.988H303.996V173.988H251.996Z" fill="#17181C"/>
|
||||
<path d="M96 225.998V173.998H148V225.998H96Z" fill="#CFCECD"/>
|
||||
<rect width="52" height="52" transform="translate(148.004 173.998)" fill="#17181C"/>
|
||||
<path d="M148.004 225.998V173.998H200.004V225.998H148.004Z" fill="#17181C" fill-opacity="0.1"/>
|
||||
<path d="M200.008 225.998V173.998H252.008V225.998H200.008Z" fill="#17181C"/>
|
||||
<path d="M252.016 225.998V173.998H304.016V225.998H252.016Z" fill="#CFCECD"/>
|
||||
<rect width="52" height="52" transform="translate(96 226.002)" fill="#17181C"/>
|
||||
<path d="M96 278.002V226.002H148V278.002H96Z" fill="#17181C" fill-opacity="0.1"/>
|
||||
<rect width="52" height="52" transform="translate(148.004 226.002)" fill="white"/>
|
||||
<path d="M148.004 278.002V226.002H200.004V278.002H148.004Z" fill="#CFCECD"/>
|
||||
<path d="M200.008 278.002V226.002H252.008V278.002H200.008Z" fill="#CFCECD"/>
|
||||
<path d="M252.016 278.002V226.002H304.016V278.002H252.016Z" fill="#CFCECD"/>
|
||||
<path d="M96 330.012V278.012H148V330.012H96Z" fill="#17181C"/>
|
||||
<path d="M148.004 330.012V278.012H200.004V330.012H148.004Z" fill="#17181C"/>
|
||||
<path d="M200.008 329.99V277.99H252.008V329.99H200.008Z" fill="#17181C"/>
|
||||
<path d="M251.996 330.012V278.012H303.996V330.012H251.996Z" fill="#17181C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -1 +0,0 @@
|
||||
../../../ui/src/assets/favicon/favicon.ico
|
||||
@@ -1 +0,0 @@
|
||||
../../../ui/src/assets/favicon/favicon.svg
|
||||
|
Before Width: | Height: | Size: 42 B After Width: | Height: | Size: 269 B |
@@ -0,0 +1,4 @@
|
||||
<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: 42 B After Width: | Height: | Size: 269 B |
@@ -2,5 +2,4 @@ User-agent: *
|
||||
Allow: /
|
||||
|
||||
# Disallow shared content pages
|
||||
Disallow: /s/
|
||||
Disallow: /share/
|
||||
Disallow: /s/
|
||||
@@ -1 +0,0 @@
|
||||
../../../ui/src/assets/favicon/site.webmanifest
|
||||
@@ -1 +0,0 @@
|
||||
../../../ui/src/assets/favicon/web-app-manifest-192x192.png
|
||||
@@ -1 +0,0 @@
|
||||
../../../ui/src/assets/favicon/web-app-manifest-512x512.png
|
||||
@@ -1,8 +1,7 @@
|
||||
import { MetaProvider, Title, Meta } from "@solidjs/meta"
|
||||
import { Router } from "@solidjs/router"
|
||||
import { FileRoutes } from "@solidjs/start/router"
|
||||
import { Suspense } from "solid-js"
|
||||
import { Favicon } from "@opencode-ai/ui/favicon"
|
||||
import { ErrorBoundary, Suspense } from "solid-js"
|
||||
import "@ibm/plex/css/ibm-plex.css"
|
||||
import "./app.css"
|
||||
|
||||
@@ -14,7 +13,6 @@ export default function App() {
|
||||
<MetaProvider>
|
||||
<Title>opencode</Title>
|
||||
<Meta name="description" content="OpenCode - The AI coding agent built for the terminal." />
|
||||
<Favicon />
|
||||
<Suspense>{props.children}</Suspense>
|
||||
</MetaProvider>
|
||||
)}
|
||||
|
||||
@@ -202,7 +202,7 @@ export function IconZai(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconGemini(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
export function IconGoogle(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<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>
|
||||
|
||||
@@ -9,6 +9,7 @@ export default createHandler(
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.svg" />
|
||||
<meta property="og:image" content="/social-share.png" />
|
||||
<meta property="twitter:image" content="/social-share.png" />
|
||||
{assets}
|
||||
|
||||
@@ -19,7 +19,6 @@ export async function GET(input: APIEvent) {
|
||||
return {
|
||||
...value,
|
||||
account: {
|
||||
...value.account,
|
||||
[id]: {
|
||||
id,
|
||||
email: decoded.subject.properties.email,
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
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")
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -45,6 +45,7 @@ export default function Home() {
|
||||
{/*<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>
|
||||
<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 name="twitter:image" content="/social-share.png" />
|
||||
<div data-component="container">
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
|
||||
[data-slot="item"] {
|
||||
color: var(--color-danger);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { action } from "@solidjs/router"
|
||||
import { action, redirect } from "@solidjs/router"
|
||||
import { getRequestEvent } from "solid-js/web"
|
||||
import { useAuthSession } from "~/context/auth.session"
|
||||
import { Dropdown } from "~/component/dropdown"
|
||||
@@ -17,15 +17,18 @@ const logout = action(async () => {
|
||||
event!.locals.actor = undefined
|
||||
return val
|
||||
})
|
||||
}, "auth.logout")
|
||||
throw redirect("/zen")
|
||||
})
|
||||
|
||||
export function UserMenu(props: { email: string | null | undefined }) {
|
||||
return (
|
||||
<div data-component="user-menu">
|
||||
<Dropdown trigger={props.email ?? ""} align="right">
|
||||
<a href="/auth/logout" data-slot="item">
|
||||
Logout
|
||||
</a>
|
||||
<form action={logout} method="post">
|
||||
<button type="submit" formaction={logout} data-slot="item">
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import { UserMenu } from "./user-menu"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { User } from "@opencode-ai/console-core/user.js"
|
||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { Link } from "@solidjs/meta"
|
||||
|
||||
const getUserEmail = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
@@ -21,6 +22,7 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
|
||||
const userEmail = createAsync(() => getUserEmail(params.id!))
|
||||
return (
|
||||
<main data-page="workspace">
|
||||
<Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" />
|
||||
<header data-component="workspace-header">
|
||||
<div data-slot="header-brand">
|
||||
<A href="/" data-component="site-title">
|
||||
|
||||
@@ -8,7 +8,7 @@ import { querySessionInfo } from "../common"
|
||||
import {
|
||||
IconAlibaba,
|
||||
IconAnthropic,
|
||||
IconGemini,
|
||||
IconGoogle,
|
||||
IconMoonshotAI,
|
||||
IconOpenAI,
|
||||
IconStealth,
|
||||
@@ -117,7 +117,7 @@ export function ModelSection() {
|
||||
case "Anthropic":
|
||||
return <IconAnthropic width={16} height={16} />
|
||||
case "Google":
|
||||
return <IconGemini width={16} height={16} />
|
||||
return <IconGoogle width={16} height={16} />
|
||||
case "Moonshot AI":
|
||||
return <IconMoonshotAI width={16} height={16} />
|
||||
case "Z.ai":
|
||||
|
||||
@@ -8,7 +8,6 @@ import styles from "./provider-section.module.css"
|
||||
const PROVIDERS = [
|
||||
{ name: "OpenAI", key: "openai", prefix: "sk-" },
|
||||
{ name: "Anthropic", key: "anthropic", prefix: "sk-ant-" },
|
||||
{ name: "Google Gemini", key: "google", prefix: "AI" },
|
||||
] as const
|
||||
|
||||
type Provider = (typeof PROVIDERS)[number]
|
||||
|
||||
@@ -50,10 +50,6 @@ export function UsageSection() {
|
||||
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 usage = await getUsageInfo(params.id!, store.page - 1)
|
||||
setStore({
|
||||
@@ -99,11 +95,8 @@ export function UsageSection() {
|
||||
{(usage, index) => {
|
||||
const date = createMemo(() => new Date(usage.timeCreated))
|
||||
const totalInputTokens = createMemo(() => calculateTotalInputTokens(usage))
|
||||
const totalOutputTokens = createMemo(() => calculateTotalOutputTokens(usage))
|
||||
const inputBreakdownId = `input-breakdown-${index()}`
|
||||
const outputBreakdownId = `output-breakdown-${index()}`
|
||||
const isInputOpen = createMemo(() => openBreakdownId() === inputBreakdownId)
|
||||
const isOutputOpen = createMemo(() => openBreakdownId() === outputBreakdownId)
|
||||
const breakdownId = `breakdown-${index()}`
|
||||
const isOpen = createMemo(() => openBreakdownId() === breakdownId)
|
||||
const isClaude = usage.model.toLowerCase().includes("claude")
|
||||
return (
|
||||
<tr>
|
||||
@@ -117,13 +110,13 @@ export function UsageSection() {
|
||||
data-slot="breakdown-button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setOpenBreakdownId(isInputOpen() ? null : inputBreakdownId)
|
||||
setOpenBreakdownId(isOpen() ? null : breakdownId)
|
||||
}}
|
||||
>
|
||||
<IconBreakdown />
|
||||
</button>
|
||||
<span onClick={() => setOpenBreakdownId(null)}>{totalInputTokens()}</span>
|
||||
<Show when={isInputOpen()}>
|
||||
<Show when={isOpen()}>
|
||||
<div data-slot="breakdown-popup" onClick={(e) => e.stopPropagation()}>
|
||||
<div data-slot="breakdown-row">
|
||||
<span data-slot="breakdown-label">Input</span>
|
||||
@@ -143,32 +136,7 @@ export function UsageSection() {
|
||||
</Show>
|
||||
</div>
|
||||
</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-tokens">{usage.outputTokens}</td>
|
||||
<td data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</td>
|
||||
</tr>
|
||||
)
|
||||
|
||||
@@ -18,24 +18,23 @@ import { Legal } from "~/component/legal"
|
||||
import { Footer } from "~/component/footer"
|
||||
import { Header } from "~/component/header"
|
||||
import { getLastSeenWorkspaceID } from "../workspace/common"
|
||||
import { IconGemini, IconZai } from "~/component/icon"
|
||||
|
||||
const checkLoggedIn = query(async () => {
|
||||
"use server"
|
||||
const workspaceID = await getLastSeenWorkspaceID().catch(() => {})
|
||||
const workspaceID = await getLastSeenWorkspaceID()
|
||||
if (workspaceID) throw redirect(`/workspace/${workspaceID}`)
|
||||
}, "checkLoggedIn.get")
|
||||
|
||||
export default function Home() {
|
||||
const loggedin = createAsync(() => checkLoggedIn())
|
||||
createAsync(() => checkLoggedIn())
|
||||
return (
|
||||
<main data-page="zen">
|
||||
{/*<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>
|
||||
<Link rel="canonical" href={`${config.baseUrl}/zen`} />
|
||||
<Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" />
|
||||
<Meta property="og: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">
|
||||
<Header zen />
|
||||
@@ -82,9 +81,6 @@ export default function Home() {
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<IconGemini width="24" height="24" />
|
||||
</div>
|
||||
<div>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
@@ -115,9 +111,6 @@ export default function Home() {
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<IconZai width="24" height="24" />
|
||||
</div>
|
||||
</div>
|
||||
<a href="/auth">
|
||||
<span>Get started with Zen </span>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
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,7 +19,6 @@ import { googleHelper } from "./provider/google"
|
||||
import { openaiHelper } from "./provider/openai"
|
||||
import { oaCompatHelper } from "./provider/openai-compatible"
|
||||
import { createRateLimiter } from "./rateLimiter"
|
||||
import { createDataDumper } from "./dataDumper"
|
||||
|
||||
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
|
||||
type RetryOptions = {
|
||||
@@ -49,24 +48,21 @@ export async function handler(
|
||||
try {
|
||||
const url = input.request.url
|
||||
const body = await input.request.json()
|
||||
const ip = input.request.headers.get("x-real-ip") ?? ""
|
||||
const model = opts.parseModel(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({
|
||||
is_tream: isStream,
|
||||
session: sessionId,
|
||||
request: requestId,
|
||||
session: input.request.headers.get("x-opencode-session"),
|
||||
request: input.request.headers.get("x-opencode-request"),
|
||||
})
|
||||
const zenData = ZenData.list()
|
||||
const modelInfo = validateModel(zenData, model)
|
||||
const dataDumper = createDataDumper(sessionId, requestId)
|
||||
const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
|
||||
await rateLimiter?.check()
|
||||
|
||||
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
|
||||
const providerInfo = selectProvider(zenData, modelInfo, sessionId, retry)
|
||||
const providerInfo = selectProvider(zenData, modelInfo, ip, retry)
|
||||
const authInfo = await authenticate(modelInfo, providerInfo)
|
||||
validateBilling(authInfo, modelInfo)
|
||||
validateModelSettings(authInfo)
|
||||
@@ -108,14 +104,10 @@ export async function handler(
|
||||
})
|
||||
}
|
||||
|
||||
return { providerInfo, authInfo, reqBody, res, startTimestamp }
|
||||
return { providerInfo, authInfo, res, startTimestamp }
|
||||
}
|
||||
|
||||
const { providerInfo, authInfo, reqBody, res, startTimestamp } = await retriableRequest()
|
||||
|
||||
// Store model request
|
||||
dataDumper?.provideModel(providerInfo.storeModel)
|
||||
dataDumper?.provideRequest(reqBody)
|
||||
const { providerInfo, authInfo, res, startTimestamp } = await retriableRequest()
|
||||
|
||||
// Scrub response headers
|
||||
const resHeaders = new Headers()
|
||||
@@ -134,8 +126,6 @@ export async function handler(
|
||||
const body = JSON.stringify(responseConverter(json))
|
||||
logger.metric({ response_length: body.length })
|
||||
logger.debug("RESPONSE: " + body)
|
||||
dataDumper?.provideResponse(body)
|
||||
dataDumper?.flush()
|
||||
await rateLimiter?.track()
|
||||
await trackUsage(authInfo, modelInfo, providerInfo, json.usage)
|
||||
await reload(authInfo)
|
||||
@@ -165,7 +155,6 @@ export async function handler(
|
||||
response_length: responseLength,
|
||||
"timestamp.last_byte": Date.now(),
|
||||
})
|
||||
dataDumper?.flush()
|
||||
await rateLimiter?.track()
|
||||
const usage = usageParser.retrieve()
|
||||
if (usage) {
|
||||
@@ -185,7 +174,6 @@ export async function handler(
|
||||
}
|
||||
responseLength += value.length
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
dataDumper?.provideStream(buffer)
|
||||
|
||||
const parts = buffer.split(providerInfo.streamSeparator)
|
||||
buffer = parts.pop() ?? ""
|
||||
@@ -275,7 +263,7 @@ export async function handler(
|
||||
return { id: modelId, ...modelData }
|
||||
}
|
||||
|
||||
function selectProvider(zenData: ZenData, modelInfo: ModelInfo, sessionId: string, retry: RetryOptions) {
|
||||
function selectProvider(zenData: ZenData, modelInfo: ModelInfo, ip: string, retry: RetryOptions) {
|
||||
const provider = (() => {
|
||||
if (retry.retryCount === MAX_RETRIES) {
|
||||
return modelInfo.providers.find((provider) => provider.id === modelInfo.fallbackProvider)
|
||||
@@ -286,13 +274,9 @@ export async function handler(
|
||||
.filter((provider) => !retry.excludeProviders.includes(provider.id))
|
||||
.flatMap((provider) => Array<typeof provider>(provider.weight ?? 1).fill(provider))
|
||||
|
||||
// Use the last 4 characters of session ID to select a provider
|
||||
let h = 0
|
||||
const l = sessionId.length
|
||||
for (let i = l - 4; i < l; i++) {
|
||||
h = (h * 31 + sessionId.charCodeAt(i)) | 0 // 32-bit int
|
||||
}
|
||||
const index = (h >>> 0) % providers.length // make unsigned + range 0..length-1
|
||||
// Use the last 2 characters of IP address to select a provider
|
||||
const lastChars = ip.slice(-2)
|
||||
const index = parseInt(lastChars, 16) % providers.length
|
||||
return providers[index || 0]
|
||||
})()
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.112",
|
||||
"version": "1.0.101",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
@@ -30,7 +30,6 @@
|
||||
"update-models": "script/update-models.ts",
|
||||
"promote-models-to-dev": "script/promote-models.ts dev",
|
||||
"promote-models-to-prod": "script/promote-models.ts production",
|
||||
"pull-models-from-dev": "script/pull-models.ts dev",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/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}`
|
||||
@@ -32,7 +32,6 @@ export namespace ZenData {
|
||||
model: z.string(),
|
||||
weight: z.number().optional(),
|
||||
disabled: z.boolean().optional(),
|
||||
storeModel: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
@@ -74,14 +74,6 @@ declare module "sst" {
|
||||
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
|
||||
@@ -120,8 +112,6 @@ declare module "sst" {
|
||||
AuthApi: cloudflare.Service
|
||||
AuthStorage: cloudflare.KVNamespace
|
||||
Bucket: cloudflare.R2Bucket
|
||||
ConsoleData: cloudflare.R2Bucket
|
||||
EnterpriseStorage: cloudflare.R2Bucket
|
||||
GatewayKv: cloudflare.KVNamespace
|
||||
LogProcessor: cloudflare.Service
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.112",
|
||||
"version": "1.0.101",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -74,14 +74,6 @@ declare module "sst" {
|
||||
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
|
||||
@@ -120,8 +112,6 @@ declare module "sst" {
|
||||
AuthApi: cloudflare.Service
|
||||
AuthStorage: cloudflare.KVNamespace
|
||||
Bucket: cloudflare.R2Bucket
|
||||
ConsoleData: cloudflare.R2Bucket
|
||||
EnterpriseStorage: cloudflare.R2Bucket
|
||||
GatewayKv: cloudflare.KVNamespace
|
||||
LogProcessor: cloudflare.Service
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.112",
|
||||
"version": "1.0.101",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { env } from "cloudflare:workers"
|
||||
export { waitUntil } from "cloudflare:workers"
|
||||
|
||||
export const Resource = new Proxy(
|
||||
{},
|
||||
|
||||
@@ -2,66 +2,54 @@ import type { KVNamespaceListOptions, KVNamespaceListResult, KVNamespacePutOptio
|
||||
import { Resource as ResourceBase } from "sst"
|
||||
import Cloudflare from "cloudflare"
|
||||
|
||||
export const waitUntil = async (fn: () => Promise<void>) => {
|
||||
await fn()
|
||||
}
|
||||
|
||||
export const Resource = new Proxy(
|
||||
{},
|
||||
{
|
||||
get(_target, prop: keyof typeof ResourceBase) {
|
||||
const value = ResourceBase[prop]
|
||||
if ("type" in value) {
|
||||
// @ts-ignore
|
||||
if ("type" in value && value.type === "sst.cloudflare.Kv") {
|
||||
const client = new Cloudflare({
|
||||
apiToken: ResourceBase.CLOUDFLARE_API_TOKEN.value,
|
||||
})
|
||||
// @ts-ignore
|
||||
if (value.type === "sst.cloudflare.Bucket") {
|
||||
return {
|
||||
put: async () => {},
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
if (value.type === "sst.cloudflare.Kv") {
|
||||
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, {
|
||||
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,
|
||||
value: v,
|
||||
expiration: opts?.expiration,
|
||||
expiration_ttl: opts?.expirationTtl,
|
||||
metadata: opts?.metadata,
|
||||
}),
|
||||
delete: (k: string) =>
|
||||
client.kv.namespaces.values.delete(namespaceId, k, {
|
||||
})
|
||||
.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,
|
||||
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,
|
||||
}
|
||||
}),
|
||||
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
|
||||
|
||||
@@ -74,14 +74,6 @@ declare module "sst" {
|
||||
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
|
||||
@@ -120,8 +112,6 @@ declare module "sst" {
|
||||
AuthApi: cloudflare.Service
|
||||
AuthStorage: cloudflare.KVNamespace
|
||||
Bucket: cloudflare.R2Bucket
|
||||
ConsoleData: cloudflare.R2Bucket
|
||||
EnterpriseStorage: cloudflare.R2Bucket
|
||||
GatewayKv: cloudflare.KVNamespace
|
||||
LogProcessor: cloudflare.Service
|
||||
}
|
||||
|
||||
@@ -3,15 +3,9 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>OpenCode</title>
|
||||
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta property="og:image" content="/social-share.png" />
|
||||
<meta property="twitter:image" content="/social-share.png" />
|
||||
<link rel="shortcut icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>OpenCode</title>
|
||||
</head>
|
||||
<body class="antialiased overscroll-none select-none text-12-regular">
|
||||
<script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.112",
|
||||
"version": "1.0.101",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../ui/src/assets/favicon/apple-touch-icon.png
|
||||
@@ -1 +0,0 @@
|
||||
../../ui/src/assets/favicon/favicon-96x96.png
|
||||
@@ -1 +0,0 @@
|
||||
../../ui/src/assets/favicon/favicon.ico
|
||||
@@ -1 +0,0 @@
|
||||
../../ui/src/assets/favicon/site.webmanifest
|
||||
|
Before Width: | Height: | Size: 8.7 KiB |
@@ -1 +0,0 @@
|
||||
../../ui/src/assets/favicon/web-app-manifest-192x192.png
|
||||
@@ -1 +0,0 @@
|
||||
../../ui/src/assets/favicon/web-app-manifest-512x512.png
|
||||
@@ -5,7 +5,7 @@ import { useSync } from "./sync"
|
||||
import { makePersisted } from "@solid-primitives/storage"
|
||||
import { TextSelection } from "./local"
|
||||
import { pipe, sumBy } from "remeda"
|
||||
import { AssistantMessage, UserMessage } from "@opencode-ai/sdk"
|
||||
import { AssistantMessage } from "@opencode-ai/sdk"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { base64Encode } from "@/utils"
|
||||
|
||||
@@ -123,8 +123,8 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
|
||||
user: userMessages,
|
||||
last: lastUserMessage,
|
||||
active: activeMessage,
|
||||
setActive(message: UserMessage | undefined) {
|
||||
setStore("messageId", message?.id)
|
||||
setActive(id: string | undefined) {
|
||||
setStore("messageId", id)
|
||||
},
|
||||
},
|
||||
usage: {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../ui/src/custom-elements.d.ts
|
||||
@@ -3,8 +3,7 @@ import "@/index.css"
|
||||
import { render } from "solid-js/web"
|
||||
import { Router, Route, Navigate } from "@solidjs/router"
|
||||
import { MetaProvider } from "@solidjs/meta"
|
||||
import { Font } from "@opencode-ai/ui/font"
|
||||
import { Favicon } from "@opencode-ai/ui/favicon"
|
||||
import { Fonts } from "@opencode-ai/ui/fonts"
|
||||
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
|
||||
import { GlobalSyncProvider, useGlobalSync } from "./context/global-sync"
|
||||
import Layout from "@/pages/layout"
|
||||
@@ -39,7 +38,7 @@ render(
|
||||
<GlobalSyncProvider>
|
||||
<LayoutProvider>
|
||||
<MetaProvider>
|
||||
<Font />
|
||||
<Fonts />
|
||||
<Router root={Layout}>
|
||||
<Route
|
||||
path="/"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { For, onCleanup, onMount, Show, Match, Switch, createResource, createMemo } from "solid-js"
|
||||
import { For, onCleanup, onMount, Show, Match, Switch, createResource } from "solid-js"
|
||||
import { useLocal, type LocalFile } from "@/context/local"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { getDirectory, getFilename } from "@/utils"
|
||||
@@ -12,8 +12,7 @@ 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 { SessionTimeline } from "@opencode-ai/ui/session-timeline"
|
||||
import { SessionReview } from "@opencode-ai/ui/session-review"
|
||||
import { SelectDialog } from "@opencode-ai/ui/select-dialog"
|
||||
import {
|
||||
@@ -256,8 +255,6 @@ export default function Page() {
|
||||
return typeof draggable.id === "string" ? draggable.id : undefined
|
||||
}
|
||||
|
||||
const wide = createMemo(() => layout.review.state() === "tab" || !session.diffs().length)
|
||||
|
||||
return (
|
||||
<div class="relative bg-background-base size-full overflow-x-hidden">
|
||||
<DragDropProvider
|
||||
@@ -333,40 +330,14 @@ export default function Page() {
|
||||
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-2xl 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-xl mx-auto">
|
||||
<Switch>
|
||||
<Match when={session.id}>
|
||||
<div class="flex items-start justify-start h-full min-h-0">
|
||||
<Show when={session.messages.user().length > 1}>
|
||||
<>
|
||||
<MessageNav
|
||||
class="@6xl:hidden mt-3 mr-8"
|
||||
messages={session.messages.user()}
|
||||
current={session.messages.active()}
|
||||
onMessageSelect={session.messages.setActive}
|
||||
size="compact"
|
||||
working={session.working()}
|
||||
/>
|
||||
<MessageNav
|
||||
classList={{
|
||||
"hidden @6xl:flex": true,
|
||||
"mt-0.5 mr-3 absolute right-full": wide(),
|
||||
"mt-3 mr-8": !wide(),
|
||||
}}
|
||||
messages={session.messages.user()}
|
||||
current={session.messages.active()}
|
||||
onMessageSelect={session.messages.setActive}
|
||||
size={wide() ? "normal" : "compact"}
|
||||
working={session.working()}
|
||||
/>
|
||||
</>
|
||||
</Show>
|
||||
<SessionTurn
|
||||
sessionID={session.id!}
|
||||
messageID={session.messages.active()?.id!}
|
||||
classes={{ root: "pb-20 flex-1 min-w-0", content: "pb-20" }}
|
||||
/>
|
||||
</div>
|
||||
<SessionTimeline
|
||||
sessionID={session.id!}
|
||||
expanded={layout.review.state() === "tab" || !session.diffs().length}
|
||||
classes={{ root: "pb-20", container: "pb-20" }}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch">
|
||||
@@ -401,15 +372,10 @@ export default function Page() {
|
||||
<Show when={layout.review.state() === "pane" && session.diffs().length}>
|
||||
<div
|
||||
classList={{
|
||||
"relative grow pt-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
|
||||
classes={{
|
||||
root: "pb-20",
|
||||
header: "px-6",
|
||||
container: "px-6",
|
||||
}}
|
||||
diffs={session.diffs()}
|
||||
actions={
|
||||
<Tooltip value="Open in tab">
|
||||
@@ -432,18 +398,10 @@ export default function Page() {
|
||||
<Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden">
|
||||
<div
|
||||
classList={{
|
||||
"relative pt-3 flex-1 min-h-0 overflow-hidden": true,
|
||||
"relative px-6 py-3 flex-1 min-h-0 overflow-hidden": true,
|
||||
}}
|
||||
>
|
||||
<SessionReview
|
||||
classes={{
|
||||
root: "pb-40",
|
||||
header: "px-6",
|
||||
container: "px-6",
|
||||
}}
|
||||
diffs={session.diffs()}
|
||||
split
|
||||
/>
|
||||
<SessionReview diffs={session.diffs()} split class="pb-40" />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Show>
|
||||
@@ -498,7 +456,7 @@ export default function Page() {
|
||||
</DragOverlay>
|
||||
</DragDropProvider>
|
||||
<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-8">
|
||||
<div class="absolute inset-x-0 px-6 max-w-2xl flex flex-col justify-center items-center z-50 mx-auto bottom-6">
|
||||
<PromptInput
|
||||
ref={(el) => {
|
||||
inputRef = el
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.112",
|
||||
"version": "1.0.101",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -13,8 +13,6 @@
|
||||
"dependencies": {
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"aws4fetch": "^1.0.20",
|
||||
"@pierre/precision-diffs": "catalog:",
|
||||
"@solidjs/router": "catalog:",
|
||||
"@solidjs/start": "catalog:",
|
||||
"@solidjs/meta": "catalog:",
|
||||
@@ -26,7 +24,6 @@
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "catalog:",
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"@types/luxon": "catalog:",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../ui/src/assets/favicon/apple-touch-icon.png
|
||||
@@ -1 +0,0 @@
|
||||
../../ui/src/assets/favicon/favicon-96x96.png
|
||||
@@ -1 +0,0 @@
|
||||
../../ui/src/assets/favicon/favicon.ico
|
||||
|
Before Width: | Height: | Size: 39 B After Width: | Height: | Size: 664 B |
|
Before Width: | Height: | Size: 39 B After Width: | Height: | Size: 664 B |
@@ -1 +0,0 @@
|
||||
../../ui/src/assets/favicon/favicon.svg
|
||||
@@ -1 +0,0 @@
|
||||
../../ui/src/assets/favicon/site.webmanifest
|
||||
|
Before Width: | Height: | Size: 8.7 KiB |
@@ -1 +0,0 @@
|
||||
../../ui/src/assets/favicon/web-app-manifest-192x192.png
|
||||
@@ -1 +0,0 @@
|
||||
../../ui/src/assets/favicon/web-app-manifest-512x512.png
|
||||
@@ -1 +1,18 @@
|
||||
@import "@opencode-ai/ui/styles/tailwind";
|
||||
|
||||
:root {
|
||||
--background-rgb: 214, 219, 220;
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background-rgb: 0, 0, 0;
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
/* background: rgb(var(--background-rgb)); */
|
||||
/* color: rgb(var(--foreground-rgb)); */
|
||||
}
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import { Router } from "@solidjs/router"
|
||||
import { FileRoutes } from "@solidjs/start/router"
|
||||
import { Font } from "@opencode-ai/ui/font"
|
||||
import { Suspense } from "solid-js"
|
||||
import { Fonts } from "@opencode-ai/ui/fonts"
|
||||
import { MetaProvider } from "@solidjs/meta"
|
||||
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
|
||||
import { Suspense } from "solid-js"
|
||||
import "./app.css"
|
||||
import { Favicon } from "@opencode-ai/ui/favicon"
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Router
|
||||
root={(props) => (
|
||||
<MetaProvider>
|
||||
<MarkedProvider>
|
||||
<Favicon />
|
||||
<Font />
|
||||
<Suspense>{props.children}</Suspense>
|
||||
</MarkedProvider>
|
||||
</MetaProvider>
|
||||
<>
|
||||
<Suspense>
|
||||
<MarkedProvider>
|
||||
<MetaProvider>
|
||||
<Fonts />
|
||||
{props.children}
|
||||
</MetaProvider>
|
||||
</MarkedProvider>
|
||||
</Suspense>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<FileRoutes />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FileDiff, Message, Model, Part, Session, SessionStatus } from "@opencode-ai/sdk"
|
||||
import { FileDiff, Message, 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"
|
||||
@@ -8,7 +8,6 @@ export namespace Share {
|
||||
export const Info = z.object({
|
||||
id: z.string(),
|
||||
secret: z.string(),
|
||||
sessionID: z.string(),
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
@@ -30,16 +29,15 @@ export namespace Share {
|
||||
data: z.custom<FileDiff[]>(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("model"),
|
||||
data: z.custom<Model[]>(),
|
||||
type: z.literal("session_status"),
|
||||
data: z.custom<SessionStatus>(),
|
||||
}),
|
||||
])
|
||||
export type Data = z.infer<typeof Data>
|
||||
|
||||
export const create = fn(z.object({ sessionID: z.string() }), async (body) => {
|
||||
export const create = fn(Info.pick({ id: true }), async (body) => {
|
||||
const info: Info = {
|
||||
id: body.sessionID.slice(-8),
|
||||
sessionID: body.sessionID,
|
||||
id: body.id,
|
||||
secret: crypto.randomUUID(),
|
||||
}
|
||||
const exists = await get(info.id)
|
||||
@@ -49,8 +47,8 @@ export namespace Share {
|
||||
return info
|
||||
})
|
||||
|
||||
export async function get(id: string) {
|
||||
return Storage.read<Info>(["share", id])
|
||||
async function get(sessionID: string) {
|
||||
return Storage.read<Info>(["share", sessionID])
|
||||
}
|
||||
|
||||
export const remove = fn(Info.pick({ id: true, secret: true }), async (body) => {
|
||||
@@ -64,8 +62,8 @@ export namespace Share {
|
||||
}
|
||||
})
|
||||
|
||||
export async function data(id: string) {
|
||||
const list = await Storage.list(["share_data", id])
|
||||
export async function data(sessionID: string) {
|
||||
const list = await Storage.list(["share_data", sessionID])
|
||||
const promises = []
|
||||
for (const item of list) {
|
||||
promises.push(
|
||||
@@ -83,7 +81,7 @@ export namespace Share {
|
||||
|
||||
export const sync = fn(
|
||||
z.object({
|
||||
share: Info.pick({ id: true, secret: true }),
|
||||
share: Info,
|
||||
data: Data.array(),
|
||||
}),
|
||||
async (input) => {
|
||||
@@ -110,8 +108,8 @@ export namespace Share {
|
||||
case "session_diff":
|
||||
await Storage.write(["share_data", input.share.id, "session_diff"], item.data)
|
||||
break
|
||||
case "model":
|
||||
await Storage.write(["share_data", input.share.id, "model"], item.data)
|
||||
case "session_status":
|
||||
await Storage.write(["share_data", input.share.id, "session_status"], item.data)
|
||||
break
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { AwsClient } from "aws4fetch"
|
||||
import {
|
||||
S3Client,
|
||||
PutObjectCommand,
|
||||
GetObjectCommand,
|
||||
DeleteObjectCommand,
|
||||
ListObjectsV2Command,
|
||||
} from "@aws-sdk/client-s3"
|
||||
import { lazy } from "@opencode-ai/util/lazy"
|
||||
|
||||
export namespace Storage {
|
||||
@@ -9,68 +15,82 @@ export namespace Storage {
|
||||
list(prefix: string): Promise<string[]>
|
||||
}
|
||||
|
||||
function createAdapter(client: AwsClient, endpoint: string, bucket: string): Adapter {
|
||||
const base = `${endpoint}/${bucket}`
|
||||
function createAdapter(client: S3Client, bucket: string): Adapter {
|
||||
return {
|
||||
async read(path: string): Promise<string | undefined> {
|
||||
const response = await client.fetch(`${base}/${path}`)
|
||||
if (response.status === 404) return undefined
|
||||
if (!response.ok) throw new Error(`Failed to read ${path}: ${response.status}`)
|
||||
return response.text()
|
||||
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 response = await client.fetch(`${base}/${path}`, {
|
||||
method: "PUT",
|
||||
body: value,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: path,
|
||||
Body: value,
|
||||
ContentType: "application/json",
|
||||
})
|
||||
if (!response.ok) throw new Error(`Failed to write ${path}: ${response.status}`)
|
||||
await client.send(command)
|
||||
},
|
||||
|
||||
async remove(path: string): Promise<void> {
|
||||
const response = await client.fetch(`${base}/${path}`, {
|
||||
method: "DELETE",
|
||||
const command = new DeleteObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: path,
|
||||
})
|
||||
if (!response.ok) throw new Error(`Failed to remove ${path}: ${response.status}`)
|
||||
await client.send(command)
|
||||
},
|
||||
|
||||
async list(prefix: string): Promise<string[]> {
|
||||
const params = new URLSearchParams({ "list-type": "2", prefix })
|
||||
const response = await client.fetch(`${base}?${params}`)
|
||||
if (!response.ok) throw new Error(`Failed to list ${prefix}: ${response.status}`)
|
||||
const xml = await response.text()
|
||||
const keys: string[] = []
|
||||
const regex = /<Key>([^<]+)<\/Key>/g
|
||||
let match
|
||||
while ((match = regex.exec(xml)) !== null) {
|
||||
keys.push(match[1])
|
||||
}
|
||||
return keys
|
||||
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 region = process.env.OPENCODE_STORAGE_REGION || "us-east-1"
|
||||
const client = new AwsClient({
|
||||
region,
|
||||
accessKeyId: process.env.OPENCODE_STORAGE_ACCESS_KEY_ID!,
|
||||
secretAccessKey: process.env.OPENCODE_STORAGE_SECRET_ACCESS_KEY!,
|
||||
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, `https://s3.${region}.amazonaws.com`, bucket)
|
||||
return createAdapter(client, bucket)
|
||||
}
|
||||
|
||||
function r2() {
|
||||
const accountId = process.env.OPENCODE_STORAGE_ACCOUNT_ID!
|
||||
const client = new AwsClient({
|
||||
accessKeyId: process.env.OPENCODE_STORAGE_ACCESS_KEY_ID!,
|
||||
secretAccessKey: process.env.OPENCODE_STORAGE_SECRET_ACCESS_KEY!,
|
||||
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, `https://${accountId}.r2.cloudflarestorage.com`, process.env.OPENCODE_STORAGE_BUCKET!)
|
||||
return createAdapter(client, bucket)
|
||||
}
|
||||
|
||||
const adapter = lazy(() => {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../ui/src/custom-elements.d.ts
|
||||
@@ -8,9 +8,8 @@ export default createHandler(() => (
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<title>OpenCode</title>
|
||||
<meta property="og:image" content="/social-share.png" />
|
||||
<meta property="twitter:image" content="/social-share.png" />
|
||||
{assets}
|
||||
</head>
|
||||
<body class="antialiased overscroll-none select-none text-12-regular">
|
||||
|
||||
@@ -37,7 +37,6 @@ app
|
||||
schema: resolver(
|
||||
z
|
||||
.object({
|
||||
id: z.string(),
|
||||
url: z.string(),
|
||||
secret: z.string(),
|
||||
})
|
||||
@@ -51,18 +50,17 @@ app
|
||||
validator("json", z.object({ sessionID: z.string() })),
|
||||
async (c) => {
|
||||
const body = c.req.valid("json")
|
||||
const share = await Share.create({ sessionID: body.sessionID })
|
||||
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({
|
||||
id: share.id,
|
||||
secret: share.secret,
|
||||
url: `${protocol}://${host}/share/${share.id}`,
|
||||
})
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/share/:shareID/sync",
|
||||
"/share/:sessionID/sync",
|
||||
describeRoute({
|
||||
description: "Sync share data",
|
||||
operationId: "share.sync",
|
||||
@@ -77,20 +75,20 @@ app
|
||||
},
|
||||
},
|
||||
}),
|
||||
validator("param", z.object({ shareID: z.string() })),
|
||||
validator("param", z.object({ sessionID: z.string() })),
|
||||
validator("json", z.object({ secret: z.string(), data: Share.Data.array() })),
|
||||
async (c) => {
|
||||
const { shareID } = c.req.valid("param")
|
||||
const { sessionID } = c.req.valid("param")
|
||||
const body = c.req.valid("json")
|
||||
await Share.sync({
|
||||
share: { id: shareID, secret: body.secret },
|
||||
share: { id: sessionID, secret: body.secret },
|
||||
data: body.data,
|
||||
})
|
||||
return c.json({})
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/share/:shareID/data",
|
||||
"/share/:sessionID/data",
|
||||
describeRoute({
|
||||
description: "Get share data",
|
||||
operationId: "share.data",
|
||||
@@ -105,14 +103,14 @@ app
|
||||
},
|
||||
},
|
||||
}),
|
||||
validator("param", z.object({ shareID: z.string() })),
|
||||
validator("param", z.object({ sessionID: z.string() })),
|
||||
async (c) => {
|
||||
const { shareID } = c.req.valid("param")
|
||||
return c.json(await Share.data(shareID))
|
||||
const { sessionID } = c.req.valid("param")
|
||||
return c.json(await Share.data(sessionID))
|
||||
},
|
||||
)
|
||||
.delete(
|
||||
"/share/:shareID",
|
||||
"/share/:sessionID",
|
||||
describeRoute({
|
||||
description: "Remove a share",
|
||||
operationId: "share.remove",
|
||||
@@ -127,12 +125,12 @@ app
|
||||
},
|
||||
},
|
||||
}),
|
||||
validator("param", z.object({ shareID: z.string() })),
|
||||
validator("param", z.object({ sessionID: z.string() })),
|
||||
validator("json", z.object({ secret: z.string() })),
|
||||
async (c) => {
|
||||
const { shareID } = c.req.valid("param")
|
||||
const { sessionID } = c.req.valid("param")
|
||||
const body = c.req.valid("json")
|
||||
await Share.remove({ id: shareID, secret: body.secret })
|
||||
await Share.remove({ id: sessionID, secret: body.secret })
|
||||
return c.json({})
|
||||
},
|
||||
)
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { FileDiff, Message, Part, Session, SessionStatus } from "@opencode-ai/sdk"
|
||||
import { SessionTimeline } from "@opencode-ai/ui/session-timeline"
|
||||
import { SessionReview } from "@opencode-ai/ui/session-review"
|
||||
import { DataProvider, useData } from "@opencode-ai/ui/context"
|
||||
import { createAsync, query, RouteDefinition, useParams } from "@solidjs/router"
|
||||
import { createMemo, Show } 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 { DateTime } from "luxon"
|
||||
|
||||
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[]
|
||||
}
|
||||
} = {
|
||||
session: [],
|
||||
session_diff: {
|
||||
[sessionID]: [],
|
||||
},
|
||||
session_status: {
|
||||
[sessionID]: {
|
||||
type: "idle",
|
||||
},
|
||||
},
|
||||
message: {},
|
||||
part: {},
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
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) return
|
||||
return getData(params.sessionID)
|
||||
})
|
||||
|
||||
return (
|
||||
<Show when={data()}>
|
||||
{(data) => (
|
||||
<DataProvider data={data()}>
|
||||
{iife(() => {
|
||||
const data = useData()
|
||||
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 firstUserMessage = createMemo(() =>
|
||||
data.message[params.sessionID!]?.filter((m) => m.role === "user")?.at(0),
|
||||
)
|
||||
const provider = createMemo(() => firstUserMessage()?.model?.providerID)
|
||||
const model = createMemo(() => firstUserMessage()?.model?.modelID)
|
||||
const diffs = createMemo(() => data.session_diff[params.sessionID!] ?? [])
|
||||
|
||||
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="w-full flex-1 min-h-0 flex">
|
||||
<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,
|
||||
}}
|
||||
>
|
||||
<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-4 shrink-0 dark:invert"
|
||||
/>
|
||||
<div class="text-12-regular text-text-base">{model()}</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>
|
||||
<SessionTimeline
|
||||
sessionID={params.sessionID!}
|
||||
classes={{ root: "grow", content: "flex flex-col justify-between", container: "pb-20" }}
|
||||
expanded
|
||||
>
|
||||
<div class="flex items-center justify-center pb-8 shrink-0">
|
||||
<Logo class="w-58.5 opacity-12" />
|
||||
</div>
|
||||
</SessionTimeline>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</DataProvider>
|
||||
)}
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
@@ -1,360 +0,0 @@
|
||||
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, useParams } from "@solidjs/router"
|
||||
import { createEffect, 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"
|
||||
import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr"
|
||||
|
||||
const SessionDataMissingError = NamedError.create(
|
||||
"SessionDataMissingError",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
message: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
|
||||
const getData = query(async (shareID) => {
|
||||
"use server"
|
||||
const share = await Share.get(shareID)
|
||||
if (!share) throw new SessionDataMissingError({ sessionID: shareID })
|
||||
const data = await Share.data(shareID)
|
||||
const result: {
|
||||
sessionID: string
|
||||
session: Session[]
|
||||
session_diff: {
|
||||
[sessionID: string]: FileDiff[]
|
||||
}
|
||||
session_diff_preload: {
|
||||
[sessionID: string]: PreloadMultiFileDiffResult<any>[]
|
||||
}
|
||||
session_status: {
|
||||
[sessionID: string]: SessionStatus
|
||||
}
|
||||
message: {
|
||||
[sessionID: string]: Message[]
|
||||
}
|
||||
part: {
|
||||
[messageID: string]: Part[]
|
||||
}
|
||||
model: {
|
||||
[sessionID: string]: Model[]
|
||||
}
|
||||
} = {
|
||||
sessionID: share.sessionID,
|
||||
session: [],
|
||||
session_diff: {
|
||||
[share.sessionID]: [],
|
||||
},
|
||||
session_diff_preload: {
|
||||
[share.sessionID]: [],
|
||||
},
|
||||
session_status: {
|
||||
[share.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[share.sessionID] = item.data
|
||||
result.session_diff_preload[share.sessionID] = await Promise.all(
|
||||
item.data.map(async (diff) =>
|
||||
preloadMultiFileDiff<any>({
|
||||
oldFile: { name: diff.file, contents: diff.before },
|
||||
newFile: { name: diff.file, contents: diff.after },
|
||||
options: {
|
||||
theme: "OpenCode",
|
||||
themeType: "system",
|
||||
disableLineNumbers: false,
|
||||
overflow: "wrap",
|
||||
diffStyle: "unified",
|
||||
diffIndicators: "bars",
|
||||
disableBackground: false,
|
||||
expansionLineCount: 20,
|
||||
lineDiffType: "none",
|
||||
maxLineDiffLength: 1000,
|
||||
maxLineLengthForHighlighting: 1000,
|
||||
disableFileHeader: true,
|
||||
},
|
||||
// annotations,
|
||||
}),
|
||||
),
|
||||
)
|
||||
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[share.sessionID] = item.data
|
||||
break
|
||||
}
|
||||
}
|
||||
const match = Binary.search(result.session, share.sessionID, (s) => s.id)
|
||||
if (!match.found) throw new SessionDataMissingError({ sessionID: share.sessionID })
|
||||
return result
|
||||
}, "getShareData")
|
||||
|
||||
export default function () {
|
||||
const params = useParams()
|
||||
const data = createAsync(async () => {
|
||||
if (!params.shareID) throw new Error("Missing shareID")
|
||||
return getData(params.shareID)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
console.log(data())
|
||||
})
|
||||
|
||||
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, data().sessionID, (s) => s.id))
|
||||
if (!match().found) throw new Error(`Session ${data().sessionID} not found`)
|
||||
const info = createMemo(() => data().session[match().index])
|
||||
const messages = createMemo(() =>
|
||||
data().sessionID
|
||||
? (data().message[data().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[data().sessionID]?.find((m) => m.id === modelID()))
|
||||
const diffs = createMemo(() => {
|
||||
const diffs = data().session_diff[data().sessionID] ?? []
|
||||
const preloaded = data().session_diff_preload[data().sessionID] ?? []
|
||||
return diffs.map((diff) => ({
|
||||
...diff,
|
||||
preloaded: preloaded.find((d) => d.newFile.name === diff.file),
|
||||
}))
|
||||
})
|
||||
|
||||
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={data().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>
|
||||
)
|
||||
|
||||
const wide = createMemo(() => diffs().length === 0)
|
||||
|
||||
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": !wide(),
|
||||
"px-6 max-w-2xl": wide(),
|
||||
}}
|
||||
>
|
||||
{title()}
|
||||
<div class="flex items-start justify-start h-full min-h-0">
|
||||
<Show when={messages().length > 1}>
|
||||
<>
|
||||
<div class="md:hidden absolute right-full">
|
||||
<MessageNav
|
||||
class="mt-2 mr-3"
|
||||
messages={messages()}
|
||||
current={activeMessage()}
|
||||
onMessageSelect={setActiveMessage}
|
||||
size="compact"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
classList={{
|
||||
"hidden md:block": true,
|
||||
"absolute right-[90%]": !wide(),
|
||||
"absolute right-full": wide(),
|
||||
}}
|
||||
>
|
||||
<MessageNav
|
||||
classList={{
|
||||
"mt-2.5 mr-3": !wide(),
|
||||
"mt-0.5 mr-8": wide(),
|
||||
}}
|
||||
messages={messages()}
|
||||
current={activeMessage()}
|
||||
onMessageSelect={setActiveMessage}
|
||||
size={wide() ? "normal" : "compact"}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</Show>
|
||||
<SessionTurn
|
||||
sessionID={data().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 > 0}>
|
||||
<div class="relative grow pt-14 flex-1 min-h-0 border-l border-border-weak-base">
|
||||
<SessionReview
|
||||
diffs={diffs()}
|
||||
classes={{
|
||||
root: "pb-20",
|
||||
header: "px-6",
|
||||
container: "px-6",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Switch>
|
||||
<Match when={diffs().length > 0}>
|
||||
<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 !border-r-0" classes={{ button: "w-full" }}>
|
||||
5 Files Changed
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="session" class="!overflow-hidden">
|
||||
{turns()}
|
||||
</Tabs.Content>
|
||||
<Tabs.Content forceMount value="review" class="!overflow-hidden hidden data-[selected]:block">
|
||||
<div class="relative h-full pt-8 overflow-y-auto no-scrollbar">
|
||||
<SessionReview
|
||||
diffs={diffs()}
|
||||
classes={{
|
||||
root: "pb-20",
|
||||
header: "px-4",
|
||||
container: "px-4",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<div class="md:hidden !overflow-hidden">{turns()}</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</DataProvider>
|
||||
)}
|
||||
</Show>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
/* 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 {}
|
||||
@@ -11,7 +11,7 @@
|
||||
"allowJs": true,
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"types": ["@cloudflare/workers-types", "vite/client"],
|
||||
"types": ["vite/client"],
|
||||
"isolatedModules": true,
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
|
||||
@@ -3,7 +3,7 @@ import { solidStart } from "@solidjs/start/config"
|
||||
import { nitro } from "nitro/vite"
|
||||
import tailwindcss from "@tailwindcss/vite"
|
||||
|
||||
const nitroConfig: any = (() => {
|
||||
const nitroConfig = (() => {
|
||||
const target = process.env.OPENCODE_DEPLOYMENT_TARGET
|
||||
if (target === "cloudflare") {
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The AI coding agent built for the terminal"
|
||||
version = "1.0.112"
|
||||
version = "1.0.101"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/sst/opencode"
|
||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.112/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.101/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.112/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.101/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.112/opencode-linux-arm64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.101/opencode-linux-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.112/opencode-linux-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.101/opencode-linux-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.112/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.101/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.112",
|
||||
"version": "1.0.101",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -74,14 +74,6 @@ declare module "sst" {
|
||||
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
|
||||
@@ -120,8 +112,6 @@ declare module "sst" {
|
||||
AuthApi: cloudflare.Service
|
||||
AuthStorage: cloudflare.KVNamespace
|
||||
Bucket: cloudflare.R2Bucket
|
||||
ConsoleData: cloudflare.R2Bucket
|
||||
EnterpriseStorage: cloudflare.R2Bucket
|
||||
GatewayKv: cloudflare.KVNamespace
|
||||
LogProcessor: cloudflare.Service
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.0.112",
|
||||
"version": "1.0.101",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
@@ -18,6 +18,8 @@
|
||||
"./*": "./src/*.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ai-sdk/amazon-bedrock": "2.2.10",
|
||||
"@ai-sdk/google-vertex": "3.0.16",
|
||||
"@babel/core": "7.28.4",
|
||||
"@octokit/webhooks-types": "7.6.1",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
@@ -42,14 +44,7 @@
|
||||
"@actions/core": "1.11.1",
|
||||
"@actions/github": "6.0.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/openai": "2.0.71",
|
||||
"@ai-sdk/openai-compatible": "1.0.27",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@hono/standard-validator": "0.1.5",
|
||||
"@hono/zod-validator": "catalog:",
|
||||
@@ -61,9 +56,8 @@
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "1.2.5",
|
||||
"@opentui/core": "0.1.50",
|
||||
"@opentui/solid": "0.1.50",
|
||||
"@opentui/core": "0.1.47",
|
||||
"@opentui/solid": "0.1.47",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/precision-diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
@@ -82,7 +76,6 @@
|
||||
"jsonc-parser": "3.3.1",
|
||||
"minimatch": "10.0.3",
|
||||
"open": "10.1.2",
|
||||
"opentui-spinner": "0.0.5",
|
||||
"partial-json": "0.1.7",
|
||||
"remeda": "catalog:",
|
||||
"solid-js": "catalog:",
|
||||
|
||||
@@ -2,14 +2,11 @@ import z from "zod"
|
||||
import { Global } from "../global"
|
||||
import { Log } from "../util/log"
|
||||
import path from "path"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
import { NamedError } from "../util/error"
|
||||
import { readableStreamToText } from "bun"
|
||||
import { createRequire } from "module"
|
||||
import { Lock } from "../util/lock"
|
||||
|
||||
export namespace BunProc {
|
||||
const log = Log.create({ service: "bun" })
|
||||
const req = createRequire(import.meta.url)
|
||||
|
||||
export async function run(cmd: string[], options?: Bun.SpawnOptions.OptionsObject<any, any, any>) {
|
||||
log.info("running", {
|
||||
@@ -61,9 +58,6 @@ export namespace BunProc {
|
||||
)
|
||||
|
||||
export async function install(pkg: string, version = "latest") {
|
||||
// Use lock to ensure only one install at a time
|
||||
using _ = await Lock.write("bun-install")
|
||||
|
||||
const mod = path.join(Global.Path.cache, "node_modules", pkg)
|
||||
const pkgjson = Bun.file(path.join(Global.Path.cache, "package.json"))
|
||||
const parsed = await pkgjson.json().catch(async () => {
|
||||
@@ -131,22 +125,4 @@ export namespace BunProc {
|
||||
await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2))
|
||||
return mod
|
||||
}
|
||||
|
||||
export async function resolve(pkg: string) {
|
||||
const local = workspace(pkg)
|
||||
if (local) return local
|
||||
const dir = path.join(Global.Path.cache, "node_modules", pkg)
|
||||
const pkgjson = Bun.file(path.join(dir, "package.json"))
|
||||
const exists = await pkgjson.exists()
|
||||
if (exists) return dir
|
||||
}
|
||||
|
||||
function workspace(pkg: string) {
|
||||
try {
|
||||
const target = req.resolve(`${pkg}/package.json`)
|
||||
return path.dirname(target)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Argv } from "yargs"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { Provider } from "../../provider/provider"
|
||||
import { ModelsDev } from "../../provider/models"
|
||||
import { cmd } from "./cmd"
|
||||
import { UI } from "../ui"
|
||||
import { EOL } from "os"
|
||||
@@ -20,17 +19,8 @@ export const ModelsCommand = cmd({
|
||||
describe: "use more verbose model output (includes metadata like costs)",
|
||||
type: "boolean",
|
||||
})
|
||||
.option("refresh", {
|
||||
describe: "refresh the models cache from models.dev",
|
||||
type: "boolean",
|
||||
})
|
||||
},
|
||||
handler: async (args) => {
|
||||
if (args.refresh) {
|
||||
await ModelsDev.refresh()
|
||||
UI.println(UI.Style.TEXT_SUCCESS_BOLD + "Models cache refreshed" + UI.Style.TEXT_NORMAL)
|
||||
}
|
||||
|
||||
await Instance.provide({
|
||||
directory: process.cwd(),
|
||||
async fn() {
|
||||
@@ -38,8 +28,7 @@ export const ModelsCommand = cmd({
|
||||
|
||||
function printModels(providerID: string, verbose?: boolean) {
|
||||
const provider = providers[providerID]
|
||||
const sortedModels = Object.entries(provider.info.models).sort(([a], [b]) => a.localeCompare(b))
|
||||
for (const [modelID, model] of sortedModels) {
|
||||
for (const [modelID, model] of Object.entries(provider.info.models)) {
|
||||
process.stdout.write(`${providerID}/${modelID}`)
|
||||
process.stdout.write(EOL)
|
||||
if (verbose) {
|
||||
@@ -60,15 +49,7 @@ export const ModelsCommand = cmd({
|
||||
return
|
||||
}
|
||||
|
||||
const providerIDs = Object.keys(providers).sort((a, b) => {
|
||||
const aIsOpencode = a.startsWith("opencode")
|
||||
const bIsOpencode = b.startsWith("opencode")
|
||||
if (aIsOpencode && !bIsOpencode) return -1
|
||||
if (!aIsOpencode && bIsOpencode) return 1
|
||||
return a.localeCompare(b)
|
||||
})
|
||||
|
||||
for (const providerID of providerIDs) {
|
||||
for (const providerID of Object.keys(providers)) {
|
||||
printModels(providerID, args.verbose)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -26,8 +26,6 @@ interface SessionStats {
|
||||
}
|
||||
days: number
|
||||
costPerDay: number
|
||||
tokensPerSession: number
|
||||
medianTokensPerSession: number
|
||||
}
|
||||
|
||||
export const StatsCommand = cmd({
|
||||
@@ -118,8 +116,6 @@ async function aggregateSessionStats(days?: number, projectFilter?: string): Pro
|
||||
},
|
||||
days: 0,
|
||||
costPerDay: 0,
|
||||
tokensPerSession: 0,
|
||||
medianTokensPerSession: 0,
|
||||
}
|
||||
|
||||
if (filteredSessions.length > 1000) {
|
||||
@@ -133,8 +129,6 @@ async function aggregateSessionStats(days?: number, projectFilter?: string): Pro
|
||||
let earliestTime = Date.now()
|
||||
let latestTime = 0
|
||||
|
||||
const sessionTotalTokens: number[] = []
|
||||
|
||||
const BATCH_SIZE = 20
|
||||
for (let i = 0; i < filteredSessions.length; i += BATCH_SIZE) {
|
||||
const batch = filteredSessions.slice(i, i + BATCH_SIZE)
|
||||
@@ -170,7 +164,6 @@ async function aggregateSessionStats(days?: number, projectFilter?: string): Pro
|
||||
messageCount: messages.length,
|
||||
sessionCost,
|
||||
sessionTokens,
|
||||
sessionTotalTokens: sessionTokens.input + sessionTokens.output + sessionTokens.reasoning,
|
||||
sessionToolUsage,
|
||||
earliestTime: session.time.created,
|
||||
latestTime: session.time.updated,
|
||||
@@ -182,7 +175,6 @@ async function aggregateSessionStats(days?: number, projectFilter?: string): Pro
|
||||
for (const result of batchResults) {
|
||||
earliestTime = Math.min(earliestTime, result.earliestTime)
|
||||
latestTime = Math.max(latestTime, result.latestTime)
|
||||
sessionTotalTokens.push(result.sessionTotalTokens)
|
||||
|
||||
stats.totalMessages += result.messageCount
|
||||
stats.totalCost += result.sessionCost
|
||||
@@ -205,16 +197,6 @@ async function aggregateSessionStats(days?: number, projectFilter?: string): Pro
|
||||
}
|
||||
stats.days = actualDays
|
||||
stats.costPerDay = stats.totalCost / actualDays
|
||||
const totalTokens = stats.totalTokens.input + stats.totalTokens.output + stats.totalTokens.reasoning
|
||||
stats.tokensPerSession = filteredSessions.length > 0 ? totalTokens / filteredSessions.length : 0
|
||||
sessionTotalTokens.sort((a, b) => a - b)
|
||||
const mid = Math.floor(sessionTotalTokens.length / 2)
|
||||
stats.medianTokensPerSession =
|
||||
sessionTotalTokens.length === 0
|
||||
? 0
|
||||
: sessionTotalTokens.length % 2 === 0
|
||||
? (sessionTotalTokens[mid - 1] + sessionTotalTokens[mid]) / 2
|
||||
: sessionTotalTokens[mid]
|
||||
|
||||
return stats
|
||||
}
|
||||
@@ -245,12 +227,8 @@ export function displayStats(stats: SessionStats, toolLimit?: number) {
|
||||
console.log("├────────────────────────────────────────────────────────┤")
|
||||
const cost = isNaN(stats.totalCost) ? 0 : stats.totalCost
|
||||
const costPerDay = isNaN(stats.costPerDay) ? 0 : stats.costPerDay
|
||||
const tokensPerSession = isNaN(stats.tokensPerSession) ? 0 : stats.tokensPerSession
|
||||
console.log(renderRow("Total Cost", `$${cost.toFixed(2)}`))
|
||||
console.log(renderRow("Avg Cost/Day", `$${costPerDay.toFixed(2)}`))
|
||||
console.log(renderRow("Avg Tokens/Session", formatNumber(Math.round(tokensPerSession))))
|
||||
const medianTokensPerSession = isNaN(stats.medianTokensPerSession) ? 0 : stats.medianTokensPerSession
|
||||
console.log(renderRow("Median Tokens/Session", formatNumber(Math.round(medianTokensPerSession))))
|
||||
console.log(renderRow("Cost/Day", `$${costPerDay.toFixed(2)}`))
|
||||
console.log(renderRow("Input", formatNumber(stats.totalTokens.input)))
|
||||
console.log(renderRow("Output", formatNumber(stats.totalTokens.output)))
|
||||
console.log(renderRow("Cache Read", formatNumber(stats.totalTokens.cache.read)))
|
||||
|
||||
@@ -342,21 +342,6 @@ function App() {
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Suspend terminal",
|
||||
value: "terminal.suspend",
|
||||
keybind: "terminal_suspend",
|
||||
category: "System",
|
||||
onSelect: () => {
|
||||
process.once("SIGCONT", () => {
|
||||
renderer.resume()
|
||||
})
|
||||
|
||||
renderer.suspend()
|
||||
// pid=0 means send the signal to all processes in the process group
|
||||
process.kill(0, "SIGTSTP")
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
createEffect(() => {
|
||||
@@ -426,15 +411,6 @@ function App() {
|
||||
})
|
||||
})
|
||||
|
||||
event.on(Installation.Event.UpdateAvailable.type, (evt) => {
|
||||
toast.show({
|
||||
variant: "info",
|
||||
title: "Update Available",
|
||||
message: `OpenCode v${evt.properties.version} is available. Run 'opencode upgrade' to update manually.`,
|
||||
duration: 10000,
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<box
|
||||
width={dimensions().width}
|
||||
|
||||
@@ -14,6 +14,10 @@ export function DialogThemeList() {
|
||||
let ref: DialogSelectRef<string>
|
||||
const initial = theme.selected
|
||||
|
||||
onMount(() => {
|
||||
theme.set(Object.keys(theme.all())[0])
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
if (!confirmed) theme.set(initial)
|
||||
})
|
||||
@@ -22,7 +26,6 @@ export function DialogThemeList() {
|
||||
<DialogSelect
|
||||
title="Themes"
|
||||
options={options}
|
||||
current={initial}
|
||||
onMove={(opt) => {
|
||||
theme.set(opt.value)
|
||||
}}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createMemo, createResource, createEffect, onMount, For, Show } from "so
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useSDK } from "@tui/context/sdk"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { useTheme, selectedForeground } from "@tui/context/theme"
|
||||
import { useTheme } from "@tui/context/theme"
|
||||
import { SplitBorder } from "@tui/component/border"
|
||||
import { useCommandDialog } from "@tui/component/dialog-command"
|
||||
import { Locale } from "@/util/locale"
|
||||
@@ -81,6 +81,7 @@ export function Autocomplete(props: {
|
||||
const extmarkId = input.extmarks.create({
|
||||
start: extmarkStart,
|
||||
end: extmarkEnd,
|
||||
virtual: true,
|
||||
styleId,
|
||||
typeId: props.promptPartTypeId(),
|
||||
})
|
||||
@@ -454,7 +455,7 @@ export function Autocomplete(props: {
|
||||
{...SplitBorder}
|
||||
borderColor={theme.border}
|
||||
>
|
||||
<box backgroundColor={theme.backgroundMenu} height={height()}>
|
||||
<box backgroundColor={theme.backgroundElement} height={height()}>
|
||||
<For
|
||||
each={options()}
|
||||
fallback={
|
||||
@@ -470,11 +471,11 @@ export function Autocomplete(props: {
|
||||
backgroundColor={index() === store.selected ? theme.primary : undefined}
|
||||
flexDirection="row"
|
||||
>
|
||||
<text fg={index() === store.selected ? selectedForeground(theme) : theme.text} flexShrink={0}>
|
||||
<text fg={index() === store.selected ? theme.background : theme.text} flexShrink={0}>
|
||||
{option.display}
|
||||
</text>
|
||||
<Show when={option.description}>
|
||||
<text fg={index() === store.selected ? selectedForeground(theme) : theme.textMuted} wrapMode="none">
|
||||
<text fg={index() === store.selected ? theme.background : theme.textMuted} wrapMode="none">
|
||||
{option.description}
|
||||
</text>
|
||||
</Show>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, t, dim, fg, type KeyBinding } from "@opentui/core"
|
||||
import { createEffect, createMemo, type JSX, onMount, createSignal, onCleanup, Show, Switch, Match } from "solid-js"
|
||||
import "opentui-spinner/solid"
|
||||
import { useLocal } from "@tui/context/local"
|
||||
import { useTheme } from "@tui/context/theme"
|
||||
import { EmptyBorder } from "@tui/component/border"
|
||||
@@ -21,7 +20,7 @@ import type { FilePart } from "@opencode-ai/sdk"
|
||||
import { TuiEvent } from "../../event"
|
||||
import { iife } from "@/util/iife"
|
||||
import { Locale } from "@/util/locale"
|
||||
import { createColors, createFrames } from "../../ui/spinner.ts"
|
||||
import { Shimmer } from "../../ui/shimmer"
|
||||
|
||||
export type PromptProps = {
|
||||
sessionID?: string
|
||||
@@ -310,6 +309,7 @@ export function Prompt(props: PromptProps) {
|
||||
const extmarkId = input.extmarks.create({
|
||||
start,
|
||||
end,
|
||||
virtual: true,
|
||||
styleId,
|
||||
typeId: promptPartTypeId,
|
||||
})
|
||||
@@ -545,20 +545,8 @@ export function Prompt(props: PromptProps) {
|
||||
return local.agent.color(local.agent.current().name)
|
||||
})
|
||||
|
||||
const spinnerDef = createMemo(() => {
|
||||
const color = local.agent.color(local.agent.current().name)
|
||||
return {
|
||||
frames: createFrames({
|
||||
color,
|
||||
style: "blocks",
|
||||
inactiveFactor: 0.25,
|
||||
}),
|
||||
color: createColors({
|
||||
color,
|
||||
style: "blocks",
|
||||
inactiveFactor: 0.25,
|
||||
}),
|
||||
}
|
||||
createEffect(() => {
|
||||
renderer.setCursorColor(highlight())
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -772,7 +760,7 @@ export function Prompt(props: PromptProps) {
|
||||
ref={(r: TextareaRenderable) => (input = r)}
|
||||
onMouseDown={(r: MouseEvent) => r.target?.focus()}
|
||||
focusedBackgroundColor={theme.backgroundElement}
|
||||
cursorColor={highlight()}
|
||||
cursorColor={theme.primary}
|
||||
syntaxStyle={syntax()}
|
||||
/>
|
||||
<box flexDirection="row" flexShrink={0} paddingTop={1} gap={1}>
|
||||
@@ -803,17 +791,10 @@ export function Prompt(props: PromptProps) {
|
||||
height={1}
|
||||
border={["bottom"]}
|
||||
borderColor={theme.backgroundElement}
|
||||
customBorderChars={
|
||||
theme.background.a != 0
|
||||
? {
|
||||
...EmptyBorder,
|
||||
horizontal: "▀",
|
||||
}
|
||||
: {
|
||||
...EmptyBorder,
|
||||
horizontal: " ",
|
||||
}
|
||||
}
|
||||
customBorderChars={{
|
||||
...EmptyBorder,
|
||||
horizontal: "▀",
|
||||
}}
|
||||
/>
|
||||
</box>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
@@ -825,7 +806,7 @@ export function Prompt(props: PromptProps) {
|
||||
justifyContent={status().type === "retry" ? "space-between" : "flex-start"}
|
||||
>
|
||||
<box flexShrink={0} flexDirection="row" gap={1}>
|
||||
<spinner color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
|
||||
<Loader />
|
||||
<box flexDirection="row" gap={1} flexShrink={0}>
|
||||
{(() => {
|
||||
const retry = createMemo(() => {
|
||||
@@ -895,3 +876,38 @@ export function Prompt(props: PromptProps) {
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function Loader() {
|
||||
const FRAMES = [
|
||||
"▱▱▱▱▱▱▱",
|
||||
"▱▱▱▱▱▱▱",
|
||||
"▱▱▱▱▱▱▱",
|
||||
"▱▱▱▱▱▱▱",
|
||||
"▰▱▱▱▱▱▱",
|
||||
"▰▰▱▱▱▱▱",
|
||||
"▰▰▰▱▱▱▱",
|
||||
"▱▰▰▰▱▱▱",
|
||||
"▱▱▰▰▰▱▱",
|
||||
"▱▱▱▰▰▰▱",
|
||||
"▱▱▱▱▰▰▰",
|
||||
"▱▱▱▱▱▰▰",
|
||||
"▱▱▱▱▱▱▰",
|
||||
"▱▱▱▱▱▱▱",
|
||||
"▱▱▱▱▱▱▱",
|
||||
"▱▱▱▱▱▱▱",
|
||||
"▱▱▱▱▱▱▱",
|
||||
]
|
||||
const [frame, setFrame] = createSignal(0)
|
||||
|
||||
onMount(() => {
|
||||
const timer = setInterval(() => {
|
||||
setFrame((frame() + 1) % FRAMES.length)
|
||||
}, 100)
|
||||
onCleanup(() => {
|
||||
clearInterval(timer)
|
||||
})
|
||||
})
|
||||
|
||||
const { theme } = useTheme()
|
||||
return <text fg={theme.diffAdded}>{FRAMES[frame()]}</text>
|
||||
}
|
||||
|
||||
@@ -94,7 +94,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
const agent = agents().find((x) => x.name === name)
|
||||
if (agent?.color) return RGBA.fromHex(agent.color)
|
||||
const index = agents().findIndex((x) => x.name === name)
|
||||
if (index === -1) return colors()[0]
|
||||
return colors()[index % colors().length]
|
||||
},
|
||||
}
|
||||
|
||||