Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
502e85b903 | ||
|
|
ac1e2bfd49 | ||
|
|
b9b071c744 | ||
|
|
83186b6fed | ||
|
|
a3a239967f | ||
|
|
b4fd4bb257 | ||
|
|
eb009d5959 | ||
|
|
4a81bd0f50 | ||
|
|
bbc9142fc5 | ||
|
|
7413c2715c | ||
|
|
7579c3bb15 | ||
|
|
24683058fd | ||
|
|
bf81e3108c | ||
|
|
5ade90416e | ||
|
|
8f2a8086c0 | ||
|
|
38b70f7877 | ||
|
|
5e112a17a5 | ||
|
|
59a3e7e3cc | ||
|
|
2c93f065cb | ||
|
|
488d33c1ed | ||
|
|
de4660ac12 | ||
|
|
27ae341684 | ||
|
|
25b3846694 | ||
|
|
8e2f9f6544 | ||
|
|
76b5870f89 | ||
|
|
604891e793 | ||
|
|
5814df7eaa | ||
|
|
2509d03f42 | ||
|
|
a256df9823 | ||
|
|
af96ec5a30 | ||
|
|
55df80b80e | ||
|
|
7d11986a0a | ||
|
|
d75d90c53e | ||
|
|
35fead2eca | ||
|
|
9bb2efd3ef | ||
|
|
30ffcaa667 | ||
|
|
ba11455786 | ||
|
|
d69ba27f84 | ||
|
|
448b72d046 | ||
|
|
d96d89bcb8 | ||
|
|
7a013d4f40 | ||
|
|
e4597df3b9 | ||
|
|
2b014fcd75 | ||
|
|
c2bf6975f8 | ||
|
|
733e1f79ac | ||
|
|
2a6cbfd5fd | ||
|
|
6173b69a8b | ||
|
|
fc72cfe784 | ||
|
|
768c81cdfd | ||
|
|
bcea8ed593 | ||
|
|
f93bb1dd21 | ||
|
|
b92e8510f9 | ||
|
|
1b692ec7eb | ||
|
|
4c97e2e8bb | ||
|
|
b652198979 | ||
|
|
31c4a1d853 | ||
|
|
6afdb5c0e5 | ||
|
|
bdcf864678 | ||
|
|
0dd5039668 | ||
|
|
a0831eade1 | ||
|
|
f1dc9818b6 |
@@ -21,7 +21,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Check for duplicate issues
|
- name: Check for duplicate issues
|
||||||
env:
|
env:
|
||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
OPENCODE_PERMISSION: |
|
OPENCODE_PERMISSION: |
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -148,3 +148,4 @@
|
|||||||
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
|
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
|
||||||
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
|
| 2025-11-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-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) |
|
||||||
|
|||||||
@@ -116,6 +116,8 @@ const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
|
|||||||
// CONSOLE
|
// CONSOLE
|
||||||
////////////////
|
////////////////
|
||||||
|
|
||||||
|
const bucket = new sst.cloudflare.Bucket("ConsoleData")
|
||||||
|
|
||||||
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
|
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
|
||||||
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
||||||
|
|
||||||
@@ -132,6 +134,7 @@ new sst.cloudflare.x.SolidStart("Console", {
|
|||||||
domain,
|
domain,
|
||||||
path: "packages/console/app",
|
path: "packages/console/app",
|
||||||
link: [
|
link: [
|
||||||
|
bucket,
|
||||||
database,
|
database,
|
||||||
AUTH_API_URL,
|
AUTH_API_URL,
|
||||||
STRIPE_WEBHOOK_SECRET,
|
STRIPE_WEBHOOK_SECRET,
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { SECRET } from "./secret"
|
||||||
|
import { domain } from "./stage"
|
||||||
|
|
||||||
|
const storage = new sst.cloudflare.Bucket("EnterpriseStorage")
|
||||||
|
|
||||||
|
const enterprise = new sst.cloudflare.x.SolidStart("Enterprise", {
|
||||||
|
domain: "enterprise." + domain,
|
||||||
|
path: "packages/enterprise",
|
||||||
|
buildCommand: "bun run build:cloudflare",
|
||||||
|
environment: {
|
||||||
|
OPENCODE_STORAGE_ADAPTER: "r2",
|
||||||
|
OPENCODE_STORAGE_ACCOUNT_ID: sst.cloudflare.DEFAULT_ACCOUNT_ID,
|
||||||
|
OPENCODE_STORAGE_ACCESS_KEY_ID: SECRET.R2AccessKey.value,
|
||||||
|
OPENCODE_STORAGE_SECRET_ACCESS_KEY: SECRET.R2SecretKey.value,
|
||||||
|
OPENCODE_STORAGE_BUCKET: storage.name,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export const SECRET = {
|
||||||
|
R2AccessKey: new sst.Secret("R2AccessKey", "unknown"),
|
||||||
|
R2SecretKey: new sst.Secret("R2SecretKey", "unknown"),
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import solidPlugin from "./node_modules/@opentui/solid/scripts/solid-plugin"
|
||||||
|
import path from "path"
|
||||||
|
import fs from "fs"
|
||||||
|
|
||||||
|
const dir = process.cwd()
|
||||||
|
const parser = fs.realpathSync(path.join(dir, "node_modules/@opentui/core/parser.worker.js"))
|
||||||
|
const worker = "./src/cli/cmd/tui/worker.ts"
|
||||||
|
const version = process.env.OPENCODE_VERSION ?? "local"
|
||||||
|
const channel = process.env.OPENCODE_CHANNEL ?? "local"
|
||||||
|
|
||||||
|
fs.rmSync(path.join(dir, "dist"), { recursive: true, force: true })
|
||||||
|
|
||||||
|
const result = await Bun.build({
|
||||||
|
entrypoints: ["./src/index.ts", worker, parser],
|
||||||
|
outdir: "./dist",
|
||||||
|
target: "bun",
|
||||||
|
sourcemap: "none",
|
||||||
|
tsconfig: "./tsconfig.json",
|
||||||
|
plugins: [solidPlugin],
|
||||||
|
external: ["@opentui/core"],
|
||||||
|
define: {
|
||||||
|
OPENCODE_VERSION: `'${version}'`,
|
||||||
|
OPENCODE_CHANNEL: `'${channel}'`,
|
||||||
|
// Leave undefined so runtime picks bundled/dist worker or fallback in code.
|
||||||
|
OPENCODE_WORKER_PATH: "undefined",
|
||||||
|
OTUI_TREE_SITTER_WORKER_PATH: 'new URL("./cli/cmd/tui/parser.worker.js", import.meta.url).href',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.error("bundle failed")
|
||||||
|
for (const log of result.logs) console.error(log)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const parserOut = path.join(dir, "dist/src/cli/cmd/tui/parser.worker.js")
|
||||||
|
fs.mkdirSync(path.dirname(parserOut), { recursive: true })
|
||||||
|
await Bun.write(parserOut, Bun.file(parser))
|
||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": "sha256-N33FQyKF6IgGIRZ8NFd9o1/sjHMwbQ6KQcnMFyN0WmI="
|
"nodeModules": "sha256-/ZkyVHgRMjhzBpnDNTR6X+TomtTMarVU7gmq9Z8Czr8="
|
||||||
}
|
}
|
||||||
|
|||||||
+60
-35
@@ -1,4 +1,4 @@
|
|||||||
{ lib, stdenv, stdenvNoCC, bun, fzf, ripgrep, makeBinaryWrapper }:
|
{ lib, stdenvNoCC, bun, fzf, ripgrep, makeBinaryWrapper }:
|
||||||
args:
|
args:
|
||||||
let
|
let
|
||||||
scripts = args.scripts;
|
scripts = args.scripts;
|
||||||
@@ -28,64 +28,89 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
|||||||
makeBinaryWrapper
|
makeBinaryWrapper
|
||||||
];
|
];
|
||||||
|
|
||||||
configurePhase = ''
|
|
||||||
runHook preConfigure
|
|
||||||
cp -R ${finalAttrs.node_modules}/. .
|
|
||||||
runHook postConfigure
|
|
||||||
'';
|
|
||||||
|
|
||||||
env.MODELS_DEV_API_JSON = args.modelsDev;
|
env.MODELS_DEV_API_JSON = args.modelsDev;
|
||||||
env.OPENCODE_VERSION = args.version;
|
env.OPENCODE_VERSION = args.version;
|
||||||
env.OPENCODE_CHANNEL = "stable";
|
env.OPENCODE_CHANNEL = "stable";
|
||||||
|
dontConfigure = true;
|
||||||
|
|
||||||
buildPhase = ''
|
buildPhase = ''
|
||||||
runHook preBuild
|
runHook preBuild
|
||||||
|
|
||||||
cp ${scripts + "/bun-build.ts"} bun-build.ts
|
cp -r ${finalAttrs.node_modules}/node_modules .
|
||||||
|
cp -r ${finalAttrs.node_modules}/packages .
|
||||||
|
|
||||||
substituteInPlace bun-build.ts \
|
(
|
||||||
--replace '@VERSION@' "${finalAttrs.version}"
|
cd packages/opencode
|
||||||
|
|
||||||
export BUN_COMPILE_TARGET=${args.target}
|
chmod -R u+w ./node_modules
|
||||||
bun --bun bun-build.ts
|
mkdir -p ./node_modules/@opencode-ai
|
||||||
|
rm -f ./node_modules/@opencode-ai/{script,sdk,plugin}
|
||||||
|
ln -s $(pwd)/../../packages/script ./node_modules/@opencode-ai/script
|
||||||
|
ln -s $(pwd)/../../packages/sdk/js ./node_modules/@opencode-ai/sdk
|
||||||
|
ln -s $(pwd)/../../packages/plugin ./node_modules/@opencode-ai/plugin
|
||||||
|
|
||||||
|
cp ${./bundle.ts} ./bundle.ts
|
||||||
|
chmod +x ./bundle.ts
|
||||||
|
bun run ./bundle.ts
|
||||||
|
)
|
||||||
|
|
||||||
runHook postBuild
|
runHook postBuild
|
||||||
'';
|
'';
|
||||||
|
|
||||||
dontStrip = true;
|
|
||||||
|
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
runHook preInstall
|
runHook preInstall
|
||||||
|
|
||||||
cd packages/opencode
|
cd packages/opencode
|
||||||
if [ ! -f opencode ]; then
|
if [ ! -d dist ]; then
|
||||||
echo "ERROR: opencode binary not found in $(pwd)"
|
echo "ERROR: dist directory missing after bundle step"
|
||||||
ls -la
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [ ! -f opencode-worker.js ]; then
|
|
||||||
echo "ERROR: opencode worker bundle not found in $(pwd)"
|
|
||||||
ls -la
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
install -Dm755 opencode $out/bin/opencode
|
mkdir -p $out/lib/opencode
|
||||||
install -Dm644 opencode-worker.js $out/bin/opencode-worker.js
|
cp -r dist $out/lib/opencode/
|
||||||
if [ -f opencode-assets.manifest ]; then
|
chmod -R u+w $out/lib/opencode/dist
|
||||||
while IFS= read -r asset; do
|
|
||||||
[ -z "$asset" ] && continue
|
# Select bundled worker assets deterministically (sorted find output)
|
||||||
if [ ! -f "$asset" ]; then
|
worker_file=$(find "$out/lib/opencode/dist" -type f \( -path '*/tui/worker.*' -o -name 'worker.*' \) | sort | head -n1)
|
||||||
echo "ERROR: referenced asset \"$asset\" missing"
|
parser_worker_file=$(find "$out/lib/opencode/dist" -type f -name 'parser.worker.*' | sort | head -n1)
|
||||||
exit 1
|
if [ -z "$worker_file" ]; then
|
||||||
fi
|
echo "ERROR: bundled worker not found"
|
||||||
install -Dm644 "$asset" "$out/bin/$(basename "$asset")"
|
exit 1
|
||||||
done < opencode-assets.manifest
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
main_wasm=$(printf '%s\n' "$out"/lib/opencode/dist/tree-sitter-*.wasm | sort | head -n1)
|
||||||
|
wasm_list=$(find "$out/lib/opencode/dist" -maxdepth 1 -name 'tree-sitter-*.wasm' -print)
|
||||||
|
for patch_file in "$worker_file" "$parser_worker_file"; do
|
||||||
|
[ -z "$patch_file" ] && continue
|
||||||
|
[ ! -f "$patch_file" ] && continue
|
||||||
|
if [ -n "$wasm_list" ] && grep -q 'tree-sitter' "$patch_file"; then
|
||||||
|
# Rewrite wasm references to absolute store paths to avoid runtime resolve failures.
|
||||||
|
bun --bun ${scripts + "/patch-wasm.ts"} "$patch_file" "$main_wasm" $wasm_list
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
mkdir -p $out/lib/opencode/node_modules
|
||||||
|
cp -r ../../node_modules/.bun $out/lib/opencode/node_modules/
|
||||||
|
mkdir -p $out/lib/opencode/node_modules/@opentui
|
||||||
|
|
||||||
|
mkdir -p $out/bin
|
||||||
|
makeWrapper ${bun}/bin/bun $out/bin/opencode \
|
||||||
|
--add-flags "run" \
|
||||||
|
--add-flags "$out/lib/opencode/dist/src/index.js" \
|
||||||
|
--prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]} \
|
||||||
|
--argv0 opencode
|
||||||
|
|
||||||
runHook postInstall
|
runHook postInstall
|
||||||
'';
|
'';
|
||||||
|
|
||||||
postFixup = ''
|
postInstall = ''
|
||||||
wrapProgram "$out/bin/opencode" --prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]}
|
for pkg in $out/lib/opencode/node_modules/.bun/@opentui+core-* $out/lib/opencode/node_modules/.bun/@opentui+solid-* $out/lib/opencode/node_modules/.bun/@opentui+core@* $out/lib/opencode/node_modules/.bun/@opentui+solid@*; do
|
||||||
|
if [ -d "$pkg" ]; then
|
||||||
|
pkgName=$(basename "$pkg" | sed 's/@opentui+\([^@]*\)@.*/\1/')
|
||||||
|
ln -sf ../.bun/$(basename "$pkg")/node_modules/@opentui/$pkgName \
|
||||||
|
$out/lib/opencode/node_modules/@opentui/$pkgName
|
||||||
|
fi
|
||||||
|
done
|
||||||
'';
|
'';
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
|
|||||||
@@ -24,15 +24,13 @@ for (const entry of directories) {
|
|||||||
if (!info.isDirectory()) {
|
if (!info.isDirectory()) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const marker = entry.lastIndexOf("@")
|
const parsed = parseEntry(entry)
|
||||||
if (marker <= 0) {
|
if (!parsed) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const slug = entry.slice(0, marker).replace(/\+/g, "/")
|
const list = versions.get(parsed.name) ?? []
|
||||||
const version = entry.slice(marker + 1)
|
list.push({ dir: full, version: parsed.version, label: entry })
|
||||||
const list = versions.get(slug) ?? []
|
versions.set(parsed.name, list)
|
||||||
list.push({ dir: full, version, label: entry })
|
|
||||||
versions.set(slug, list)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const semverModule = (await import(join(bunRoot, "node_modules/semver"))) as
|
const semverModule = (await import(join(bunRoot, "node_modules/semver"))) as
|
||||||
@@ -79,6 +77,12 @@ for (const [slug, entry] of Array.from(selections.entries()).sort((a, b) => a[0]
|
|||||||
await mkdir(parent, { recursive: true })
|
await mkdir(parent, { recursive: true })
|
||||||
const linkPath = join(parent, leaf)
|
const linkPath = join(parent, leaf)
|
||||||
const desired = join(entry.dir, "node_modules", slug)
|
const desired = join(entry.dir, "node_modules", slug)
|
||||||
|
const exists = await lstat(desired)
|
||||||
|
.then((info) => info.isDirectory())
|
||||||
|
.catch(() => false)
|
||||||
|
if (!exists) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
const relativeTarget = relative(parent, desired)
|
const relativeTarget = relative(parent, desired)
|
||||||
const resolved = relativeTarget.length === 0 ? "." : relativeTarget
|
const resolved = relativeTarget.length === 0 ? "." : relativeTarget
|
||||||
await rm(linkPath, { recursive: true, force: true })
|
await rm(linkPath, { recursive: true, force: true })
|
||||||
@@ -94,3 +98,16 @@ for (const line of rewrites.slice(0, 20)) {
|
|||||||
if (rewrites.length > 20) {
|
if (rewrites.length > 20) {
|
||||||
console.log(" ...")
|
console.log(" ...")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseEntry(label: string) {
|
||||||
|
const marker = label.startsWith("@") ? label.indexOf("@", 1) : label.indexOf("@")
|
||||||
|
if (marker <= 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const name = label.slice(0, marker).replace(/\+/g, "/")
|
||||||
|
const version = label.slice(marker + 1)
|
||||||
|
if (!name || !version) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return { name, version }
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rewrite tree-sitter wasm references inside a JS file to absolute paths.
|
||||||
|
* argv: [node, script, file, mainWasm, ...wasmPaths]
|
||||||
|
*/
|
||||||
|
const [, , file, mainWasm, ...wasmPaths] = process.argv
|
||||||
|
|
||||||
|
if (!file || !mainWasm) {
|
||||||
|
console.error("usage: patch-wasm <file> <mainWasm> [wasmPaths...]")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fs.readFileSync(file, "utf8")
|
||||||
|
const byName = new Map<string, string>()
|
||||||
|
|
||||||
|
for (const wasm of wasmPaths) {
|
||||||
|
const name = path.basename(wasm)
|
||||||
|
byName.set(name, wasm)
|
||||||
|
}
|
||||||
|
|
||||||
|
let next = content
|
||||||
|
|
||||||
|
for (const [name, wasmPath] of byName) {
|
||||||
|
next = next.replaceAll(name, wasmPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
next = next.replaceAll("tree-sitter.wasm", mainWasm).replaceAll("web-tree-sitter/tree-sitter.wasm", mainWasm)
|
||||||
|
|
||||||
|
// Collapse any relative prefixes before absolute store paths (e.g., "../../../..//nix/store/...")
|
||||||
|
next = next.replace(/(\.\/)+/g, "./")
|
||||||
|
next = next.replace(/(\.\.\/)+\/?(\/nix\/store[^"']+)/g, "/$2")
|
||||||
|
next = next.replace(/(["'])\/{2,}(\/nix\/store[^"']+)(["'])/g, "$1/$2$3")
|
||||||
|
next = next.replace(/(["'])\/\/(nix\/store[^"']+)(["'])/g, "$1/$2$3")
|
||||||
|
|
||||||
|
if (next !== content) fs.writeFileSync(file, next)
|
||||||
+1
-4
@@ -48,7 +48,7 @@
|
|||||||
"vite": "7.1.4",
|
"vite": "7.1.4",
|
||||||
"@solidjs/meta": "0.29.4",
|
"@solidjs/meta": "0.29.4",
|
||||||
"@solidjs/router": "0.15.4",
|
"@solidjs/router": "0.15.4",
|
||||||
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dbff19d",
|
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
|
||||||
"solid-js": "1.9.10",
|
"solid-js": "1.9.10",
|
||||||
"vite-plugin-solid": "2.11.10"
|
"vite-plugin-solid": "2.11.10"
|
||||||
}
|
}
|
||||||
@@ -82,9 +82,6 @@
|
|||||||
"tree-sitter-bash",
|
"tree-sitter-bash",
|
||||||
"web-tree-sitter"
|
"web-tree-sitter"
|
||||||
],
|
],
|
||||||
"patchedDependencies": {
|
|
||||||
"@solidjs/start@1.1.7": "patches/@solidjs%2Fstart@1.1.7.patch"
|
|
||||||
},
|
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"@types/node": "catalog:"
|
"@types/node": "catalog:"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.0.102",
|
"version": "1.0.107",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export async function GET(input: APIEvent) {
|
|||||||
return {
|
return {
|
||||||
...value,
|
...value,
|
||||||
account: {
|
account: {
|
||||||
|
...value.account,
|
||||||
[id]: {
|
[id]: {
|
||||||
id,
|
id,
|
||||||
email: decoded.subject.properties.email,
|
email: decoded.subject.properties.email,
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { redirect } from "@solidjs/router"
|
||||||
|
import { APIEvent } from "@solidjs/start"
|
||||||
|
import { useAuthSession } from "~/context/auth.session"
|
||||||
|
|
||||||
|
export async function GET(event: APIEvent) {
|
||||||
|
const auth = await useAuthSession()
|
||||||
|
const current = auth.data.current
|
||||||
|
if (current)
|
||||||
|
await auth.update((val) => {
|
||||||
|
delete val.account?.[current]
|
||||||
|
const first = Object.keys(val.account ?? {})[0]
|
||||||
|
val.current = first
|
||||||
|
event!.locals.actor = undefined
|
||||||
|
return val
|
||||||
|
})
|
||||||
|
return redirect("/zen")
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { APIEvent } from "@solidjs/start"
|
||||||
|
import { useAuthSession } from "~/context/auth.session"
|
||||||
|
|
||||||
|
export async function GET(input: APIEvent) {
|
||||||
|
const session = await useAuthSession()
|
||||||
|
return Response.json(session.data)
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
[data-slot="item"] {
|
[data-slot="item"] {
|
||||||
color: var(--color-danger);
|
color: var(--color-danger);
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { action, redirect } from "@solidjs/router"
|
import { action } from "@solidjs/router"
|
||||||
import { getRequestEvent } from "solid-js/web"
|
import { getRequestEvent } from "solid-js/web"
|
||||||
import { useAuthSession } from "~/context/auth.session"
|
import { useAuthSession } from "~/context/auth.session"
|
||||||
import { Dropdown } from "~/component/dropdown"
|
import { Dropdown } from "~/component/dropdown"
|
||||||
@@ -17,18 +17,15 @@ const logout = action(async () => {
|
|||||||
event!.locals.actor = undefined
|
event!.locals.actor = undefined
|
||||||
return val
|
return val
|
||||||
})
|
})
|
||||||
throw redirect("/zen")
|
}, "auth.logout")
|
||||||
})
|
|
||||||
|
|
||||||
export function UserMenu(props: { email: string | null | undefined }) {
|
export function UserMenu(props: { email: string | null | undefined }) {
|
||||||
return (
|
return (
|
||||||
<div data-component="user-menu">
|
<div data-component="user-menu">
|
||||||
<Dropdown trigger={props.email ?? ""} align="right">
|
<Dropdown trigger={props.email ?? ""} align="right">
|
||||||
<form action={logout} method="post">
|
<a href="/auth/logout" data-slot="item">
|
||||||
<button type="submit" formaction={logout} data-slot="item">
|
Logout
|
||||||
Logout
|
</a>
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ import { getLastSeenWorkspaceID } from "../workspace/common"
|
|||||||
|
|
||||||
const checkLoggedIn = query(async () => {
|
const checkLoggedIn = query(async () => {
|
||||||
"use server"
|
"use server"
|
||||||
const workspaceID = await getLastSeenWorkspaceID()
|
const workspaceID = await getLastSeenWorkspaceID().catch(() => {})
|
||||||
if (workspaceID) throw redirect(`/workspace/${workspaceID}`)
|
if (workspaceID) throw redirect(`/workspace/${workspaceID}`)
|
||||||
}, "checkLoggedIn.get")
|
}, "checkLoggedIn.get")
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
createAsync(() => checkLoggedIn())
|
const loggedin = createAsync(() => checkLoggedIn())
|
||||||
return (
|
return (
|
||||||
<main data-page="zen">
|
<main data-page="zen">
|
||||||
{/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/}
|
{/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/}
|
||||||
@@ -35,6 +35,7 @@ export default function Home() {
|
|||||||
<Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" />
|
<Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" />
|
||||||
<Meta property="og:image" content="/social-share-zen.png" />
|
<Meta property="og:image" content="/social-share-zen.png" />
|
||||||
<Meta name="twitter:image" content="/social-share-zen.png" />
|
<Meta name="twitter:image" content="/social-share-zen.png" />
|
||||||
|
<Meta name="opencode:auth" content={loggedin() ? "true" : "false"} />
|
||||||
|
|
||||||
<div data-component="container">
|
<div data-component="container">
|
||||||
<Header zen />
|
<Header zen />
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Resource, waitUntil } from "@opencode-ai/console-resource"
|
||||||
|
|
||||||
|
export function createDataDumper(sessionId: string, requestId: string) {
|
||||||
|
if (Resource.App.stage !== "production") return
|
||||||
|
|
||||||
|
let data: Record<string, any> = {}
|
||||||
|
let modelName: string | undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
provideModel: (model?: string) => (modelName = model),
|
||||||
|
provideRequest: (request: string) => (data.request = request),
|
||||||
|
provideResponse: (response: string) => (data.response = response),
|
||||||
|
provideStream: (chunk: string) => (data.response = (data.response ?? "") + chunk),
|
||||||
|
flush: () => {
|
||||||
|
if (!modelName) return
|
||||||
|
|
||||||
|
const str = new Date().toISOString().replace(/[^0-9]/g, "")
|
||||||
|
const yyyymmdd = str.substring(0, 8)
|
||||||
|
const hh = str.substring(8, 10)
|
||||||
|
|
||||||
|
waitUntil(
|
||||||
|
Resource.ConsoleData.put(`${yyyymmdd}/${hh}/${modelName}/${sessionId}/${requestId}.json`, JSON.stringify(data)),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import { googleHelper } from "./provider/google"
|
|||||||
import { openaiHelper } from "./provider/openai"
|
import { openaiHelper } from "./provider/openai"
|
||||||
import { oaCompatHelper } from "./provider/openai-compatible"
|
import { oaCompatHelper } from "./provider/openai-compatible"
|
||||||
import { createRateLimiter } from "./rateLimiter"
|
import { createRateLimiter } from "./rateLimiter"
|
||||||
|
import { createDataDumper } from "./dataDumper"
|
||||||
|
|
||||||
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
|
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
|
||||||
type RetryOptions = {
|
type RetryOptions = {
|
||||||
@@ -48,16 +49,19 @@ export async function handler(
|
|||||||
try {
|
try {
|
||||||
const url = input.request.url
|
const url = input.request.url
|
||||||
const body = await input.request.json()
|
const body = await input.request.json()
|
||||||
const ip = input.request.headers.get("x-real-ip") ?? ""
|
|
||||||
const model = opts.parseModel(url, body)
|
const model = opts.parseModel(url, body)
|
||||||
const isStream = opts.parseIsStream(url, body)
|
const isStream = opts.parseIsStream(url, body)
|
||||||
|
const ip = input.request.headers.get("x-real-ip") ?? ""
|
||||||
|
const sessionId = input.request.headers.get("x-opencode-session") ?? ""
|
||||||
|
const requestId = input.request.headers.get("x-opencode-request") ?? ""
|
||||||
logger.metric({
|
logger.metric({
|
||||||
is_tream: isStream,
|
is_tream: isStream,
|
||||||
session: input.request.headers.get("x-opencode-session"),
|
session: sessionId,
|
||||||
request: input.request.headers.get("x-opencode-request"),
|
request: requestId,
|
||||||
})
|
})
|
||||||
const zenData = ZenData.list()
|
const zenData = ZenData.list()
|
||||||
const modelInfo = validateModel(zenData, model)
|
const modelInfo = validateModel(zenData, model)
|
||||||
|
const dataDumper = createDataDumper(sessionId, requestId)
|
||||||
const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
|
const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
|
||||||
await rateLimiter?.check()
|
await rateLimiter?.check()
|
||||||
|
|
||||||
@@ -104,10 +108,14 @@ export async function handler(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return { providerInfo, authInfo, res, startTimestamp }
|
return { providerInfo, authInfo, reqBody, res, startTimestamp }
|
||||||
}
|
}
|
||||||
|
|
||||||
const { providerInfo, authInfo, res, startTimestamp } = await retriableRequest()
|
const { providerInfo, authInfo, reqBody, res, startTimestamp } = await retriableRequest()
|
||||||
|
|
||||||
|
// Store model request
|
||||||
|
dataDumper?.provideModel(providerInfo.storeModel)
|
||||||
|
dataDumper?.provideRequest(reqBody)
|
||||||
|
|
||||||
// Scrub response headers
|
// Scrub response headers
|
||||||
const resHeaders = new Headers()
|
const resHeaders = new Headers()
|
||||||
@@ -126,6 +134,8 @@ export async function handler(
|
|||||||
const body = JSON.stringify(responseConverter(json))
|
const body = JSON.stringify(responseConverter(json))
|
||||||
logger.metric({ response_length: body.length })
|
logger.metric({ response_length: body.length })
|
||||||
logger.debug("RESPONSE: " + body)
|
logger.debug("RESPONSE: " + body)
|
||||||
|
dataDumper?.provideResponse(body)
|
||||||
|
dataDumper?.flush()
|
||||||
await rateLimiter?.track()
|
await rateLimiter?.track()
|
||||||
await trackUsage(authInfo, modelInfo, providerInfo, json.usage)
|
await trackUsage(authInfo, modelInfo, providerInfo, json.usage)
|
||||||
await reload(authInfo)
|
await reload(authInfo)
|
||||||
@@ -155,6 +165,7 @@ export async function handler(
|
|||||||
response_length: responseLength,
|
response_length: responseLength,
|
||||||
"timestamp.last_byte": Date.now(),
|
"timestamp.last_byte": Date.now(),
|
||||||
})
|
})
|
||||||
|
dataDumper?.flush()
|
||||||
await rateLimiter?.track()
|
await rateLimiter?.track()
|
||||||
const usage = usageParser.retrieve()
|
const usage = usageParser.retrieve()
|
||||||
if (usage) {
|
if (usage) {
|
||||||
@@ -174,6 +185,7 @@ export async function handler(
|
|||||||
}
|
}
|
||||||
responseLength += value.length
|
responseLength += value.length
|
||||||
buffer += decoder.decode(value, { stream: true })
|
buffer += decoder.decode(value, { stream: true })
|
||||||
|
dataDumper?.provideStream(buffer)
|
||||||
|
|
||||||
const parts = buffer.split(providerInfo.streamSeparator)
|
const parts = buffer.split(providerInfo.streamSeparator)
|
||||||
buffer = parts.pop() ?? ""
|
buffer = parts.pop() ?? ""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.0.102",
|
"version": "1.0.107",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export namespace ZenData {
|
|||||||
model: z.string(),
|
model: z.string(),
|
||||||
weight: z.number().optional(),
|
weight: z.number().optional(),
|
||||||
disabled: z.boolean().optional(),
|
disabled: z.boolean().optional(),
|
||||||
|
storeModel: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|||||||
Vendored
+10
@@ -74,6 +74,14 @@ declare module "sst" {
|
|||||||
type: "sst.sst.Secret"
|
type: "sst.sst.Secret"
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
R2AccessKey: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
R2SecretKey: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
STRIPE_SECRET_KEY: {
|
STRIPE_SECRET_KEY: {
|
||||||
type: "sst.sst.Secret"
|
type: "sst.sst.Secret"
|
||||||
value: string
|
value: string
|
||||||
@@ -112,6 +120,8 @@ declare module "sst" {
|
|||||||
AuthApi: cloudflare.Service
|
AuthApi: cloudflare.Service
|
||||||
AuthStorage: cloudflare.KVNamespace
|
AuthStorage: cloudflare.KVNamespace
|
||||||
Bucket: cloudflare.R2Bucket
|
Bucket: cloudflare.R2Bucket
|
||||||
|
ConsoleData: cloudflare.R2Bucket
|
||||||
|
EnterpriseStorage: cloudflare.R2Bucket
|
||||||
GatewayKv: cloudflare.KVNamespace
|
GatewayKv: cloudflare.KVNamespace
|
||||||
LogProcessor: cloudflare.Service
|
LogProcessor: cloudflare.Service
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.0.102",
|
"version": "1.0.107",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+10
@@ -74,6 +74,14 @@ declare module "sst" {
|
|||||||
type: "sst.sst.Secret"
|
type: "sst.sst.Secret"
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
R2AccessKey: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
R2SecretKey: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
STRIPE_SECRET_KEY: {
|
STRIPE_SECRET_KEY: {
|
||||||
type: "sst.sst.Secret"
|
type: "sst.sst.Secret"
|
||||||
value: string
|
value: string
|
||||||
@@ -112,6 +120,8 @@ declare module "sst" {
|
|||||||
AuthApi: cloudflare.Service
|
AuthApi: cloudflare.Service
|
||||||
AuthStorage: cloudflare.KVNamespace
|
AuthStorage: cloudflare.KVNamespace
|
||||||
Bucket: cloudflare.R2Bucket
|
Bucket: cloudflare.R2Bucket
|
||||||
|
ConsoleData: cloudflare.R2Bucket
|
||||||
|
EnterpriseStorage: cloudflare.R2Bucket
|
||||||
GatewayKv: cloudflare.KVNamespace
|
GatewayKv: cloudflare.KVNamespace
|
||||||
LogProcessor: cloudflare.Service
|
LogProcessor: cloudflare.Service
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.0.102",
|
"version": "1.0.107",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { env } from "cloudflare:workers"
|
import { env } from "cloudflare:workers"
|
||||||
|
export { waitUntil } from "cloudflare:workers"
|
||||||
|
|
||||||
export const Resource = new Proxy(
|
export const Resource = new Proxy(
|
||||||
{},
|
{},
|
||||||
|
|||||||
@@ -2,54 +2,66 @@ import type { KVNamespaceListOptions, KVNamespaceListResult, KVNamespacePutOptio
|
|||||||
import { Resource as ResourceBase } from "sst"
|
import { Resource as ResourceBase } from "sst"
|
||||||
import Cloudflare from "cloudflare"
|
import Cloudflare from "cloudflare"
|
||||||
|
|
||||||
|
export const waitUntil = async (fn: () => Promise<void>) => {
|
||||||
|
await fn()
|
||||||
|
}
|
||||||
|
|
||||||
export const Resource = new Proxy(
|
export const Resource = new Proxy(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
get(_target, prop: keyof typeof ResourceBase) {
|
get(_target, prop: keyof typeof ResourceBase) {
|
||||||
const value = ResourceBase[prop]
|
const value = ResourceBase[prop]
|
||||||
// @ts-ignore
|
if ("type" in value) {
|
||||||
if ("type" in value && value.type === "sst.cloudflare.Kv") {
|
|
||||||
const client = new Cloudflare({
|
|
||||||
apiToken: ResourceBase.CLOUDFLARE_API_TOKEN.value,
|
|
||||||
})
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const namespaceId = value.namespaceId
|
if (value.type === "sst.cloudflare.Bucket") {
|
||||||
const accountId = ResourceBase.CLOUDFLARE_DEFAULT_ACCOUNT_ID.value
|
return {
|
||||||
return {
|
put: async () => {},
|
||||||
get: (k: string | string[]) => {
|
}
|
||||||
const isMulti = Array.isArray(k)
|
}
|
||||||
return client.kv.namespaces
|
// @ts-ignore
|
||||||
.bulkGet(namespaceId, {
|
if (value.type === "sst.cloudflare.Kv") {
|
||||||
keys: Array.isArray(k) ? k : [k],
|
const client = new Cloudflare({
|
||||||
|
apiToken: ResourceBase.CLOUDFLARE_API_TOKEN.value,
|
||||||
|
})
|
||||||
|
// @ts-ignore
|
||||||
|
const namespaceId = value.namespaceId
|
||||||
|
const accountId = ResourceBase.CLOUDFLARE_DEFAULT_ACCOUNT_ID.value
|
||||||
|
return {
|
||||||
|
get: (k: string | string[]) => {
|
||||||
|
const isMulti = Array.isArray(k)
|
||||||
|
return client.kv.namespaces
|
||||||
|
.bulkGet(namespaceId, {
|
||||||
|
keys: Array.isArray(k) ? k : [k],
|
||||||
|
account_id: accountId,
|
||||||
|
})
|
||||||
|
.then((result) => (isMulti ? new Map(Object.entries(result?.values ?? {})) : result?.values?.[k]))
|
||||||
|
},
|
||||||
|
put: (k: string, v: string, opts?: KVNamespacePutOptions) =>
|
||||||
|
client.kv.namespaces.values.update(namespaceId, k, {
|
||||||
account_id: accountId,
|
account_id: accountId,
|
||||||
})
|
value: v,
|
||||||
.then((result) => (isMulti ? new Map(Object.entries(result?.values ?? {})) : result?.values?.[k]))
|
expiration: opts?.expiration,
|
||||||
},
|
expiration_ttl: opts?.expirationTtl,
|
||||||
put: (k: string, v: string, opts?: KVNamespacePutOptions) =>
|
metadata: opts?.metadata,
|
||||||
client.kv.namespaces.values.update(namespaceId, k, {
|
|
||||||
account_id: accountId,
|
|
||||||
value: v,
|
|
||||||
expiration: opts?.expiration,
|
|
||||||
expiration_ttl: opts?.expirationTtl,
|
|
||||||
metadata: opts?.metadata,
|
|
||||||
}),
|
|
||||||
delete: (k: string) =>
|
|
||||||
client.kv.namespaces.values.delete(namespaceId, k, {
|
|
||||||
account_id: accountId,
|
|
||||||
}),
|
|
||||||
list: (opts?: KVNamespaceListOptions): Promise<KVNamespaceListResult<unknown, string>> =>
|
|
||||||
client.kv.namespaces.keys
|
|
||||||
.list(namespaceId, {
|
|
||||||
account_id: accountId,
|
|
||||||
prefix: opts?.prefix ?? undefined,
|
|
||||||
})
|
|
||||||
.then((result) => {
|
|
||||||
return {
|
|
||||||
keys: result.result,
|
|
||||||
list_complete: true,
|
|
||||||
cacheStatus: null,
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
|
delete: (k: string) =>
|
||||||
|
client.kv.namespaces.values.delete(namespaceId, k, {
|
||||||
|
account_id: accountId,
|
||||||
|
}),
|
||||||
|
list: (opts?: KVNamespaceListOptions): Promise<KVNamespaceListResult<unknown, string>> =>
|
||||||
|
client.kv.namespaces.keys
|
||||||
|
.list(namespaceId, {
|
||||||
|
account_id: accountId,
|
||||||
|
prefix: opts?.prefix ?? undefined,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
return {
|
||||||
|
keys: result.result,
|
||||||
|
list_complete: true,
|
||||||
|
cacheStatus: null,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
|
|||||||
+10
@@ -74,6 +74,14 @@ declare module "sst" {
|
|||||||
type: "sst.sst.Secret"
|
type: "sst.sst.Secret"
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
R2AccessKey: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
R2SecretKey: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
STRIPE_SECRET_KEY: {
|
STRIPE_SECRET_KEY: {
|
||||||
type: "sst.sst.Secret"
|
type: "sst.sst.Secret"
|
||||||
value: string
|
value: string
|
||||||
@@ -112,6 +120,8 @@ declare module "sst" {
|
|||||||
AuthApi: cloudflare.Service
|
AuthApi: cloudflare.Service
|
||||||
AuthStorage: cloudflare.KVNamespace
|
AuthStorage: cloudflare.KVNamespace
|
||||||
Bucket: cloudflare.R2Bucket
|
Bucket: cloudflare.R2Bucket
|
||||||
|
ConsoleData: cloudflare.R2Bucket
|
||||||
|
EnterpriseStorage: cloudflare.R2Bucket
|
||||||
GatewayKv: cloudflare.KVNamespace
|
GatewayKv: cloudflare.KVNamespace
|
||||||
LogProcessor: cloudflare.Service
|
LogProcessor: cloudflare.Service
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.0.102",
|
"version": "1.0.107",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useSync } from "./sync"
|
|||||||
import { makePersisted } from "@solid-primitives/storage"
|
import { makePersisted } from "@solid-primitives/storage"
|
||||||
import { TextSelection } from "./local"
|
import { TextSelection } from "./local"
|
||||||
import { pipe, sumBy } from "remeda"
|
import { pipe, sumBy } from "remeda"
|
||||||
import { AssistantMessage } from "@opencode-ai/sdk"
|
import { AssistantMessage, UserMessage } from "@opencode-ai/sdk"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { base64Encode } from "@/utils"
|
import { base64Encode } from "@/utils"
|
||||||
|
|
||||||
@@ -123,8 +123,8 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
|
|||||||
user: userMessages,
|
user: userMessages,
|
||||||
last: lastUserMessage,
|
last: lastUserMessage,
|
||||||
active: activeMessage,
|
active: activeMessage,
|
||||||
setActive(id: string | undefined) {
|
setActive(message: UserMessage | undefined) {
|
||||||
setStore("messageId", id)
|
setStore("messageId", message?.id)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
usage: {
|
usage: {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { For, onCleanup, onMount, Show, Match, Switch, createResource } from "solid-js"
|
import { For, onCleanup, onMount, Show, Match, Switch, createResource, createMemo } from "solid-js"
|
||||||
import { useLocal, type LocalFile } from "@/context/local"
|
import { useLocal, type LocalFile } from "@/context/local"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { getDirectory, getFilename } from "@/utils"
|
import { getDirectory, getFilename } from "@/utils"
|
||||||
@@ -12,7 +12,8 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
|||||||
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
|
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { Code } from "@opencode-ai/ui/code"
|
import { Code } from "@opencode-ai/ui/code"
|
||||||
import { SessionTimeline } from "@opencode-ai/ui/session-timeline"
|
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
||||||
|
import { MessageNav } from "@opencode-ai/ui/message-nav"
|
||||||
import { SessionReview } from "@opencode-ai/ui/session-review"
|
import { SessionReview } from "@opencode-ai/ui/session-review"
|
||||||
import { SelectDialog } from "@opencode-ai/ui/select-dialog"
|
import { SelectDialog } from "@opencode-ai/ui/select-dialog"
|
||||||
import {
|
import {
|
||||||
@@ -255,6 +256,8 @@ export default function Page() {
|
|||||||
return typeof draggable.id === "string" ? draggable.id : undefined
|
return typeof draggable.id === "string" ? draggable.id : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const wide = createMemo(() => layout.review.state() === "tab" || !session.diffs().length)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="relative bg-background-base size-full overflow-x-hidden">
|
<div class="relative bg-background-base size-full overflow-x-hidden">
|
||||||
<DragDropProvider
|
<DragDropProvider
|
||||||
@@ -330,14 +333,26 @@ export default function Page() {
|
|||||||
flex: layout.review.state() === "pane",
|
flex: layout.review.state() === "pane",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="relative shrink-0 px-6 py-3 flex flex-col gap-6 flex-1 min-h-0 w-full max-w-xl mx-auto">
|
<div class="relative shrink-0 px-6 py-3 flex flex-col gap-6 flex-1 min-h-0 w-full max-w-2xl mx-auto">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={session.id}>
|
<Match when={session.id}>
|
||||||
<SessionTimeline
|
<div class="flex items-start justify-start h-full min-h-0">
|
||||||
sessionID={session.id!}
|
<Show when={session.messages.user().length > 1}>
|
||||||
expanded={layout.review.state() === "tab" || !session.diffs().length}
|
<MessageNav
|
||||||
classes={{ root: "pb-20", container: "pb-20" }}
|
classList={{ "mt-1.5 mr-3": 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>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch">
|
<div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch">
|
||||||
@@ -456,7 +471,7 @@ export default function Page() {
|
|||||||
</DragOverlay>
|
</DragOverlay>
|
||||||
</DragDropProvider>
|
</DragDropProvider>
|
||||||
<Show when={session.layout.tabs.active}>
|
<Show when={session.layout.tabs.active}>
|
||||||
<div class="absolute inset-x-0 px-6 max-w-2xl flex flex-col justify-center items-center z-50 mx-auto bottom-6">
|
<div class="absolute inset-x-0 px-6 max-w-2xl flex flex-col justify-center items-center z-50 mx-auto bottom-8">
|
||||||
<PromptInput
|
<PromptInput
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
inputRef = el
|
inputRef = el
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.0.102",
|
"version": "1.0.107",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@cloudflare/workers-types": "catalog:",
|
||||||
"@tailwindcss/vite": "catalog:",
|
"@tailwindcss/vite": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"@types/luxon": "catalog:",
|
"@types/luxon": "catalog:",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FileDiff, Message, Part, Session, SessionStatus } from "@opencode-ai/sdk"
|
import { FileDiff, Message, Part, Session, SessionStatus, UserMessage } from "@opencode-ai/sdk"
|
||||||
import { SessionTimeline } from "@opencode-ai/ui/session-timeline"
|
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
||||||
import { SessionReview } from "@opencode-ai/ui/session-review"
|
import { SessionReview } from "@opencode-ai/ui/session-review"
|
||||||
import { DataProvider, useData } from "@opencode-ai/ui/context"
|
import { DataProvider, useData } from "@opencode-ai/ui/context"
|
||||||
import { createAsync, query, RouteDefinition, useParams } from "@solidjs/router"
|
import { createAsync, query, RouteDefinition, useParams } from "@solidjs/router"
|
||||||
@@ -10,6 +10,8 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
|
|||||||
import { iife } from "@opencode-ai/util/iife"
|
import { iife } from "@opencode-ai/util/iife"
|
||||||
import { Binary } from "@opencode-ai/util/binary"
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
import { DateTime } from "luxon"
|
import { DateTime } from "luxon"
|
||||||
|
import { MessageNav } from "@opencode-ai/ui/message-nav"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
|
||||||
const getData = query(async (sessionID) => {
|
const getData = query(async (sessionID) => {
|
||||||
const data = await Share.data(sessionID)
|
const data = await Share.data(sessionID)
|
||||||
@@ -40,7 +42,6 @@ const getData = query(async (sessionID) => {
|
|||||||
message: {},
|
message: {},
|
||||||
part: {},
|
part: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const item of data) {
|
for (const item of data) {
|
||||||
switch (item.type) {
|
switch (item.type) {
|
||||||
case "session":
|
case "session":
|
||||||
@@ -82,14 +83,28 @@ export default function () {
|
|||||||
<DataProvider data={data()}>
|
<DataProvider data={data()}>
|
||||||
{iife(() => {
|
{iife(() => {
|
||||||
const data = useData()
|
const data = useData()
|
||||||
|
const [store, setStore] = createStore({
|
||||||
|
messageId: undefined as string | undefined,
|
||||||
|
})
|
||||||
const match = createMemo(() => Binary.search(data.session, params.sessionID!, (s) => s.id))
|
const match = createMemo(() => Binary.search(data.session, params.sessionID!, (s) => s.id))
|
||||||
if (!match().found) throw new Error(`Session ${params.sessionID} not found`)
|
if (!match().found) throw new Error(`Session ${params.sessionID} not found`)
|
||||||
const info = createMemo(() => data.session[match().index])
|
const info = createMemo(() => data.session[match().index])
|
||||||
const firstUserMessage = createMemo(() =>
|
const messages = createMemo(() =>
|
||||||
data.message[params.sessionID!]?.filter((m) => m.role === "user")?.at(0),
|
params.sessionID ? (data.message[params.sessionID]?.filter((m) => m.role === "user") ?? []) : [],
|
||||||
)
|
)
|
||||||
const provider = createMemo(() => firstUserMessage()?.model?.providerID)
|
const firstUserMessage = createMemo(() => messages().at(0))
|
||||||
const model = createMemo(() => firstUserMessage()?.model?.modelID)
|
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 model = createMemo(() => activeMessage()?.model?.modelID)
|
||||||
const diffs = createMemo(() => data.session_diff[params.sessionID!] ?? [])
|
const diffs = createMemo(() => data.session_diff[params.sessionID!] ?? [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -145,15 +160,26 @@ export default function () {
|
|||||||
</div>
|
</div>
|
||||||
<div class="text-left text-16-medium text-text-strong">{info().title}</div>
|
<div class="text-left text-16-medium text-text-strong">{info().title}</div>
|
||||||
</div>
|
</div>
|
||||||
<SessionTimeline
|
<div class="flex items-start justify-start h-full min-h-0">
|
||||||
sessionID={params.sessionID!}
|
<Show when={messages().length > 1}>
|
||||||
classes={{ root: "grow", content: "flex flex-col justify-between", container: "pb-20" }}
|
<MessageNav
|
||||||
expanded
|
classList={{ "mt-3 mr-3": true }}
|
||||||
>
|
messages={messages()}
|
||||||
<div class="flex items-center justify-center pb-8 shrink-0">
|
current={activeMessage()}
|
||||||
<Logo class="w-58.5 opacity-12" />
|
onMessageSelect={setActiveMessage}
|
||||||
</div>
|
size={!diffs().length ? "normal" : "compact"}
|
||||||
</SessionTimeline>
|
/>
|
||||||
|
</Show>
|
||||||
|
<SessionTurn
|
||||||
|
sessionID={params.sessionID!}
|
||||||
|
messageID={store.messageId ?? firstUserMessage()!.id!}
|
||||||
|
classes={{ root: "grow", content: "flex flex-col justify-between", container: "pb-20" }}
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center pb-8 shrink-0">
|
||||||
|
<Logo class="w-58.5 opacity-12" />
|
||||||
|
</div>
|
||||||
|
</SessionTurn>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Show when={diffs().length}>
|
<Show when={diffs().length}>
|
||||||
<div class="relative grow px-6 pt-14 flex-1 min-h-0 border-l border-border-weak-base">
|
<div class="relative grow px-6 pt-14 flex-1 min-h-0 border-l border-border-weak-base">
|
||||||
|
|||||||
Vendored
+131
@@ -0,0 +1,131 @@
|
|||||||
|
/* This file is auto-generated by SST. Do not edit. */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/* deno-fmt-ignore-file */
|
||||||
|
|
||||||
|
import "sst"
|
||||||
|
declare module "sst" {
|
||||||
|
export interface Resource {
|
||||||
|
ADMIN_SECRET: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
AUTH_API_URL: {
|
||||||
|
type: "sst.sst.Linkable"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
AWS_SES_ACCESS_KEY_ID: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
AWS_SES_SECRET_ACCESS_KEY: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
CLOUDFLARE_API_TOKEN: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
CLOUDFLARE_DEFAULT_ACCOUNT_ID: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
Console: {
|
||||||
|
type: "sst.cloudflare.SolidStart"
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
Database: {
|
||||||
|
database: string
|
||||||
|
host: string
|
||||||
|
password: string
|
||||||
|
port: number
|
||||||
|
type: "sst.sst.Linkable"
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
Desktop: {
|
||||||
|
type: "sst.cloudflare.StaticSite"
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
EMAILOCTOPUS_API_KEY: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
GITHUB_APP_ID: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
GITHUB_APP_PRIVATE_KEY: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
GITHUB_CLIENT_ID_CONSOLE: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
GITHUB_CLIENT_SECRET_CONSOLE: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
GOOGLE_CLIENT_ID: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
HONEYCOMB_API_KEY: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
R2AccessKey: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
R2SecretKey: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
STRIPE_SECRET_KEY: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
STRIPE_WEBHOOK_SECRET: {
|
||||||
|
type: "sst.sst.Linkable"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
Web: {
|
||||||
|
type: "sst.cloudflare.Astro"
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
ZEN_MODELS1: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
ZEN_MODELS2: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
ZEN_MODELS3: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
ZEN_MODELS4: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// cloudflare
|
||||||
|
import * as cloudflare from "@cloudflare/workers-types"
|
||||||
|
declare module "sst" {
|
||||||
|
export interface Resource {
|
||||||
|
Api: cloudflare.Service
|
||||||
|
AuthApi: cloudflare.Service
|
||||||
|
AuthStorage: cloudflare.KVNamespace
|
||||||
|
Bucket: cloudflare.R2Bucket
|
||||||
|
ConsoleData: cloudflare.R2Bucket
|
||||||
|
EnterpriseStorage: cloudflare.R2Bucket
|
||||||
|
GatewayKv: cloudflare.KVNamespace
|
||||||
|
LogProcessor: cloudflare.Service
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
import "sst"
|
||||||
|
export {}
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"types": ["vite/client"],
|
"types": ["@cloudflare/workers-types", "vite/client"],
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": ["./src/*"]
|
"~/*": ["./src/*"]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { solidStart } from "@solidjs/start/config"
|
|||||||
import { nitro } from "nitro/vite"
|
import { nitro } from "nitro/vite"
|
||||||
import tailwindcss from "@tailwindcss/vite"
|
import tailwindcss from "@tailwindcss/vite"
|
||||||
|
|
||||||
const nitroConfig = (() => {
|
const nitroConfig: any = (() => {
|
||||||
const target = process.env.OPENCODE_DEPLOYMENT_TARGET
|
const target = process.env.OPENCODE_DEPLOYMENT_TARGET
|
||||||
if (target === "cloudflare") {
|
if (target === "cloudflare") {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
id = "opencode"
|
id = "opencode"
|
||||||
name = "OpenCode"
|
name = "OpenCode"
|
||||||
description = "The AI coding agent built for the terminal"
|
description = "The AI coding agent built for the terminal"
|
||||||
version = "1.0.102"
|
version = "1.0.107"
|
||||||
schema_version = 1
|
schema_version = 1
|
||||||
authors = ["Anomaly"]
|
authors = ["Anomaly"]
|
||||||
repository = "https://github.com/sst/opencode"
|
repository = "https://github.com/sst/opencode"
|
||||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
|||||||
icon = "./icons/opencode.svg"
|
icon = "./icons/opencode.svg"
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-aarch64]
|
[agent_servers.opencode.targets.darwin-aarch64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.102/opencode-darwin-arm64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.107/opencode-darwin-arm64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-x86_64]
|
[agent_servers.opencode.targets.darwin-x86_64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.102/opencode-darwin-x64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.107/opencode-darwin-x64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-aarch64]
|
[agent_servers.opencode.targets.linux-aarch64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.102/opencode-linux-arm64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.107/opencode-linux-arm64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-x86_64]
|
[agent_servers.opencode.targets.linux-x86_64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.102/opencode-linux-x64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.107/opencode-linux-x64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.windows-x86_64]
|
[agent_servers.opencode.targets.windows-x86_64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.102/opencode-windows-x64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.107/opencode-windows-x64.zip"
|
||||||
cmd = "./opencode.exe"
|
cmd = "./opencode.exe"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.0.102",
|
"version": "1.0.107",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
Vendored
+10
@@ -74,6 +74,14 @@ declare module "sst" {
|
|||||||
type: "sst.sst.Secret"
|
type: "sst.sst.Secret"
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
R2AccessKey: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
R2SecretKey: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
STRIPE_SECRET_KEY: {
|
STRIPE_SECRET_KEY: {
|
||||||
type: "sst.sst.Secret"
|
type: "sst.sst.Secret"
|
||||||
value: string
|
value: string
|
||||||
@@ -112,6 +120,8 @@ declare module "sst" {
|
|||||||
AuthApi: cloudflare.Service
|
AuthApi: cloudflare.Service
|
||||||
AuthStorage: cloudflare.KVNamespace
|
AuthStorage: cloudflare.KVNamespace
|
||||||
Bucket: cloudflare.R2Bucket
|
Bucket: cloudflare.R2Bucket
|
||||||
|
ConsoleData: cloudflare.R2Bucket
|
||||||
|
EnterpriseStorage: cloudflare.R2Bucket
|
||||||
GatewayKv: cloudflare.KVNamespace
|
GatewayKv: cloudflare.KVNamespace
|
||||||
LogProcessor: cloudflare.Service
|
LogProcessor: cloudflare.Service
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.0.102",
|
"version": "1.0.107",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -18,8 +18,6 @@
|
|||||||
"./*": "./src/*.ts"
|
"./*": "./src/*.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ai-sdk/amazon-bedrock": "2.2.10",
|
|
||||||
"@ai-sdk/google-vertex": "3.0.16",
|
|
||||||
"@babel/core": "7.28.4",
|
"@babel/core": "7.28.4",
|
||||||
"@octokit/webhooks-types": "7.6.1",
|
"@octokit/webhooks-types": "7.6.1",
|
||||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||||
@@ -44,7 +42,14 @@
|
|||||||
"@actions/core": "1.11.1",
|
"@actions/core": "1.11.1",
|
||||||
"@actions/github": "6.0.1",
|
"@actions/github": "6.0.1",
|
||||||
"@agentclientprotocol/sdk": "0.5.1",
|
"@agentclientprotocol/sdk": "0.5.1",
|
||||||
|
"@ai-sdk/amazon-bedrock": "3.0.57",
|
||||||
|
"@ai-sdk/anthropic": "2.0.45",
|
||||||
|
"@ai-sdk/azure": "2.0.73",
|
||||||
|
"@ai-sdk/google": "2.0.42",
|
||||||
|
"@ai-sdk/google-vertex": "3.0.74",
|
||||||
"@ai-sdk/mcp": "0.0.8",
|
"@ai-sdk/mcp": "0.0.8",
|
||||||
|
"@ai-sdk/openai": "2.0.71",
|
||||||
|
"@ai-sdk/openai-compatible": "1.0.27",
|
||||||
"@clack/prompts": "1.0.0-alpha.1",
|
"@clack/prompts": "1.0.0-alpha.1",
|
||||||
"@hono/standard-validator": "0.1.5",
|
"@hono/standard-validator": "0.1.5",
|
||||||
"@hono/zod-validator": "catalog:",
|
"@hono/zod-validator": "catalog:",
|
||||||
@@ -56,8 +61,9 @@
|
|||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@opentui/core": "0.1.47",
|
"@openrouter/ai-sdk-provider": "1.2.5",
|
||||||
"@opentui/solid": "0.1.47",
|
"@opentui/core": "0.1.49",
|
||||||
|
"@opentui/solid": "0.1.49",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@pierre/precision-diffs": "catalog:",
|
"@pierre/precision-diffs": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import { Log } from "../util/log"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { NamedError } from "../util/error"
|
import { NamedError } from "../util/error"
|
||||||
import { readableStreamToText } from "bun"
|
import { readableStreamToText } from "bun"
|
||||||
|
import { createRequire } from "module"
|
||||||
|
import { Lock } from "../util/lock"
|
||||||
|
|
||||||
export namespace BunProc {
|
export namespace BunProc {
|
||||||
const log = Log.create({ service: "bun" })
|
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>) {
|
export async function run(cmd: string[], options?: Bun.SpawnOptions.OptionsObject<any, any, any>) {
|
||||||
log.info("running", {
|
log.info("running", {
|
||||||
@@ -58,6 +61,9 @@ export namespace BunProc {
|
|||||||
)
|
)
|
||||||
|
|
||||||
export async function install(pkg: string, version = "latest") {
|
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 mod = path.join(Global.Path.cache, "node_modules", pkg)
|
||||||
const pkgjson = Bun.file(path.join(Global.Path.cache, "package.json"))
|
const pkgjson = Bun.file(path.join(Global.Path.cache, "package.json"))
|
||||||
const parsed = await pkgjson.json().catch(async () => {
|
const parsed = await pkgjson.json().catch(async () => {
|
||||||
@@ -125,4 +131,22 @@ export namespace BunProc {
|
|||||||
await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2))
|
await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2))
|
||||||
return mod
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -342,6 +342,21 @@ function App() {
|
|||||||
dialog.clear()
|
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(() => {
|
createEffect(() => {
|
||||||
@@ -411,6 +426,15 @@ 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 (
|
return (
|
||||||
<box
|
<box
|
||||||
width={dimensions().width}
|
width={dimensions().width}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { createMemo, createResource, createEffect, onMount, For, Show } from "so
|
|||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useSDK } from "@tui/context/sdk"
|
import { useSDK } from "@tui/context/sdk"
|
||||||
import { useSync } from "@tui/context/sync"
|
import { useSync } from "@tui/context/sync"
|
||||||
import { useTheme } from "@tui/context/theme"
|
import { useTheme, selectedForeground } from "@tui/context/theme"
|
||||||
import { SplitBorder } from "@tui/component/border"
|
import { SplitBorder } from "@tui/component/border"
|
||||||
import { useCommandDialog } from "@tui/component/dialog-command"
|
import { useCommandDialog } from "@tui/component/dialog-command"
|
||||||
import { Locale } from "@/util/locale"
|
import { Locale } from "@/util/locale"
|
||||||
@@ -455,7 +455,7 @@ export function Autocomplete(props: {
|
|||||||
{...SplitBorder}
|
{...SplitBorder}
|
||||||
borderColor={theme.border}
|
borderColor={theme.border}
|
||||||
>
|
>
|
||||||
<box backgroundColor={theme.backgroundElement} height={height()}>
|
<box backgroundColor={theme.backgroundMenu} height={height()}>
|
||||||
<For
|
<For
|
||||||
each={options()}
|
each={options()}
|
||||||
fallback={
|
fallback={
|
||||||
@@ -471,11 +471,11 @@ export function Autocomplete(props: {
|
|||||||
backgroundColor={index() === store.selected ? theme.primary : undefined}
|
backgroundColor={index() === store.selected ? theme.primary : undefined}
|
||||||
flexDirection="row"
|
flexDirection="row"
|
||||||
>
|
>
|
||||||
<text fg={index() === store.selected ? theme.background : theme.text} flexShrink={0}>
|
<text fg={index() === store.selected ? selectedForeground(theme) : theme.text} flexShrink={0}>
|
||||||
{option.display}
|
{option.display}
|
||||||
</text>
|
</text>
|
||||||
<Show when={option.description}>
|
<Show when={option.description}>
|
||||||
<text fg={index() === store.selected ? theme.background : theme.textMuted} wrapMode="none">
|
<text fg={index() === store.selected ? selectedForeground(theme) : theme.textMuted} wrapMode="none">
|
||||||
{option.description}
|
{option.description}
|
||||||
</text>
|
</text>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -791,10 +791,17 @@ export function Prompt(props: PromptProps) {
|
|||||||
height={1}
|
height={1}
|
||||||
border={["bottom"]}
|
border={["bottom"]}
|
||||||
borderColor={theme.backgroundElement}
|
borderColor={theme.backgroundElement}
|
||||||
customBorderChars={{
|
customBorderChars={
|
||||||
...EmptyBorder,
|
theme.background.a != 0
|
||||||
horizontal: "▀",
|
? {
|
||||||
}}
|
...EmptyBorder,
|
||||||
|
horizontal: "▀",
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
...EmptyBorder,
|
||||||
|
horizontal: " ",
|
||||||
|
}
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</box>
|
</box>
|
||||||
<box flexDirection="row" justifyContent="space-between">
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
const agent = agents().find((x) => x.name === name)
|
const agent = agents().find((x) => x.name === name)
|
||||||
if (agent?.color) return RGBA.fromHex(agent.color)
|
if (agent?.color) return RGBA.fromHex(agent.color)
|
||||||
const index = agents().findIndex((x) => x.name === name)
|
const index = agents().findIndex((x) => x.name === name)
|
||||||
|
if (index === -1) return colors()[0]
|
||||||
return colors()[index % colors().length]
|
return colors()[index % colors().length]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import { createStore, produce } from "solid-js/store"
|
|||||||
import { Global } from "@/global"
|
import { Global } from "@/global"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
|
|
||||||
type Theme = {
|
type ThemeColors = {
|
||||||
primary: RGBA
|
primary: RGBA
|
||||||
secondary: RGBA
|
secondary: RGBA
|
||||||
accent: RGBA
|
accent: RGBA
|
||||||
@@ -43,9 +43,11 @@ type Theme = {
|
|||||||
info: RGBA
|
info: RGBA
|
||||||
text: RGBA
|
text: RGBA
|
||||||
textMuted: RGBA
|
textMuted: RGBA
|
||||||
|
selectedListItemText: RGBA
|
||||||
background: RGBA
|
background: RGBA
|
||||||
backgroundPanel: RGBA
|
backgroundPanel: RGBA
|
||||||
backgroundElement: RGBA
|
backgroundElement: RGBA
|
||||||
|
backgroundMenu: RGBA
|
||||||
border: RGBA
|
border: RGBA
|
||||||
borderActive: RGBA
|
borderActive: RGBA
|
||||||
borderSubtle: RGBA
|
borderSubtle: RGBA
|
||||||
@@ -86,6 +88,27 @@ type Theme = {
|
|||||||
syntaxPunctuation: RGBA
|
syntaxPunctuation: RGBA
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Theme = ThemeColors & {
|
||||||
|
_hasSelectedListItemText: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectedForeground(theme: Theme): RGBA {
|
||||||
|
// If theme explicitly defines selectedListItemText, use it
|
||||||
|
if (theme._hasSelectedListItemText) {
|
||||||
|
return theme.selectedListItemText
|
||||||
|
}
|
||||||
|
|
||||||
|
// For transparent backgrounds, calculate contrast based on primary color
|
||||||
|
if (theme.background.a === 0) {
|
||||||
|
const { r, g, b } = theme.primary
|
||||||
|
const luminance = 0.299 * r + 0.587 * g + 0.114 * b
|
||||||
|
return luminance > 0.5 ? RGBA.fromInts(0, 0, 0) : RGBA.fromInts(255, 255, 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to background color
|
||||||
|
return theme.background
|
||||||
|
}
|
||||||
|
|
||||||
type HexColor = `#${string}`
|
type HexColor = `#${string}`
|
||||||
type RefName = string
|
type RefName = string
|
||||||
type Variant = {
|
type Variant = {
|
||||||
@@ -96,7 +119,10 @@ type ColorValue = HexColor | RefName | Variant | RGBA
|
|||||||
type ThemeJson = {
|
type ThemeJson = {
|
||||||
$schema?: string
|
$schema?: string
|
||||||
defs?: Record<string, HexColor | RefName>
|
defs?: Record<string, HexColor | RefName>
|
||||||
theme: Record<keyof Theme, ColorValue>
|
theme: Omit<Record<keyof ThemeColors, ColorValue>, "selectedListItemText" | "backgroundMenu"> & {
|
||||||
|
selectedListItemText?: ColorValue
|
||||||
|
backgroundMenu?: ColorValue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_THEMES: Record<string, ThemeJson> = {
|
export const DEFAULT_THEMES: Record<string, ThemeJson> = {
|
||||||
@@ -137,19 +163,44 @@ function resolveTheme(theme: ThemeJson, mode: "dark" | "light") {
|
|||||||
|
|
||||||
if (defs[c]) {
|
if (defs[c]) {
|
||||||
return resolveColor(defs[c])
|
return resolveColor(defs[c])
|
||||||
} else if (theme.theme[c as keyof Theme]) {
|
} else if (theme.theme[c as keyof ThemeColors] !== undefined) {
|
||||||
return resolveColor(theme.theme[c as keyof Theme])
|
return resolveColor(theme.theme[c as keyof ThemeColors]!)
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Color reference "${c}" not found in defs or theme`)
|
throw new Error(`Color reference "${c}" not found in defs or theme`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return resolveColor(c[mode])
|
return resolveColor(c[mode])
|
||||||
}
|
}
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(theme.theme).map(([key, value]) => {
|
const resolved = Object.fromEntries(
|
||||||
return [key, resolveColor(value)]
|
Object.entries(theme.theme)
|
||||||
}),
|
.filter(([key]) => key !== "selectedListItemText" && key !== "backgroundMenu")
|
||||||
) as Theme
|
.map(([key, value]) => {
|
||||||
|
return [key, resolveColor(value)]
|
||||||
|
}),
|
||||||
|
) as Partial<ThemeColors>
|
||||||
|
|
||||||
|
// Handle selectedListItemText separately since it's optional
|
||||||
|
const hasSelectedListItemText = theme.theme.selectedListItemText !== undefined
|
||||||
|
if (hasSelectedListItemText) {
|
||||||
|
resolved.selectedListItemText = resolveColor(theme.theme.selectedListItemText!)
|
||||||
|
} else {
|
||||||
|
// Backward compatibility: if selectedListItemText is not defined, use background color
|
||||||
|
// This preserves the current behavior for all existing themes
|
||||||
|
resolved.selectedListItemText = resolved.background
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle backgroundMenu - optional with fallback to backgroundElement
|
||||||
|
if (theme.theme.backgroundMenu !== undefined) {
|
||||||
|
resolved.backgroundMenu = resolveColor(theme.theme.backgroundMenu)
|
||||||
|
} else {
|
||||||
|
resolved.backgroundMenu = resolved.backgroundElement
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...resolved,
|
||||||
|
_hasSelectedListItemText: hasSelectedListItemText,
|
||||||
|
} as Theme
|
||||||
}
|
}
|
||||||
|
|
||||||
export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||||
@@ -288,11 +339,13 @@ function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJs
|
|||||||
// Text colors
|
// Text colors
|
||||||
text: fg,
|
text: fg,
|
||||||
textMuted,
|
textMuted,
|
||||||
|
selectedListItemText: bg,
|
||||||
|
|
||||||
// Background colors
|
// Background colors
|
||||||
background: bg,
|
background: bg,
|
||||||
backgroundPanel: grays[2],
|
backgroundPanel: grays[2],
|
||||||
backgroundElement: grays[3],
|
backgroundElement: grays[3],
|
||||||
|
backgroundMenu: grays[3],
|
||||||
|
|
||||||
// Border colors
|
// Border colors
|
||||||
borderSubtle: grays[6],
|
borderSubtle: grays[6],
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ export function Session() {
|
|||||||
const first = permissions()[0]
|
const first = permissions()[0]
|
||||||
if (first) {
|
if (first) {
|
||||||
const response = iife(() => {
|
const response = iife(() => {
|
||||||
|
if (evt.ctrl || evt.meta) return
|
||||||
if (evt.name === "return") return "once"
|
if (evt.name === "return") return "once"
|
||||||
if (evt.name === "a") return "always"
|
if (evt.name === "a") return "always"
|
||||||
if (evt.name === "d") return "reject"
|
if (evt.name === "d") return "reject"
|
||||||
@@ -969,8 +970,8 @@ function UserMessage(props: {
|
|||||||
fallback={
|
fallback={
|
||||||
<span style={{ fg: theme.textMuted }}>
|
<span style={{ fg: theme.textMuted }}>
|
||||||
{ctx.showTimestamps()
|
{ctx.showTimestamps()
|
||||||
? `· ${Locale.todayTimeOrDateTime(props.message.time.created)}`
|
? Locale.todayTimeOrDateTime(props.message.time.created)
|
||||||
: `· ${Locale.time(props.message.time.created)}`}
|
: Locale.time(props.message.time.created)}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -999,7 +1000,12 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
|||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const messages = createMemo(() => sync.data.message[props.message.sessionID] ?? [])
|
const messages = createMemo(() => sync.data.message[props.message.sessionID] ?? [])
|
||||||
|
|
||||||
|
const final = createMemo(() => {
|
||||||
|
return props.message.finish && !["tool-calls", "unknown"].includes(props.message.finish)
|
||||||
|
})
|
||||||
|
|
||||||
const duration = createMemo(() => {
|
const duration = createMemo(() => {
|
||||||
|
if (!final()) return 0
|
||||||
if (!props.message.time.completed) return 0
|
if (!props.message.time.completed) return 0
|
||||||
const user = messages().find((x) => x.role === "user" && x.id === props.message.parentID)
|
const user = messages().find((x) => x.role === "user" && x.id === props.message.parentID)
|
||||||
if (!user) return 0
|
if (!user) return 0
|
||||||
@@ -1038,21 +1044,13 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
|||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match
|
<Match when={props.last || final()}>
|
||||||
when={
|
|
||||||
(props.message.time.completed &&
|
|
||||||
props.parts.some(
|
|
||||||
(item) => item.type === "step-finish" && !["tool-calls", "unknown"].includes(item.reason),
|
|
||||||
)) ||
|
|
||||||
props.last
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<box paddingLeft={3}>
|
<box paddingLeft={3}>
|
||||||
<text marginTop={1}>
|
<text marginTop={1}>
|
||||||
<span style={{ fg: local.agent.color(props.message.mode) }}>▣</span>{" "}
|
<span style={{ fg: local.agent.color(props.message.mode) }}>▣</span>{" "}
|
||||||
<span style={{ fg: theme.text }}>{Locale.titlecase(props.message.mode)}</span>{" "}
|
<span style={{ fg: theme.text }}>{Locale.titlecase(props.message.mode)}</span>{" "}
|
||||||
<span style={{ fg: theme.textMuted }}>⬝{props.message.modelID}</span>
|
<span style={{ fg: theme.textMuted }}>⬝{props.message.modelID}</span>
|
||||||
<Show when={props.message.time.completed}>
|
<Show when={duration()}>
|
||||||
<span style={{ fg: theme.textMuted }}> ⬝{Locale.duration(duration())}</span>
|
<span style={{ fg: theme.textMuted }}> ⬝{Locale.duration(duration())}</span>
|
||||||
</Show>
|
</Show>
|
||||||
</text>
|
</text>
|
||||||
@@ -1100,7 +1098,7 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass
|
|||||||
|
|
||||||
function TextPart(props: { last: boolean; part: TextPart; message: AssistantMessage }) {
|
function TextPart(props: { last: boolean; part: TextPart; message: AssistantMessage }) {
|
||||||
const ctx = use()
|
const ctx = use()
|
||||||
const { syntax } = useTheme()
|
const { theme, syntax } = useTheme()
|
||||||
return (
|
return (
|
||||||
<Show when={props.part.text.trim()}>
|
<Show when={props.part.text.trim()}>
|
||||||
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0}>
|
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0}>
|
||||||
@@ -1111,6 +1109,7 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess
|
|||||||
syntaxStyle={syntax()}
|
syntaxStyle={syntax()}
|
||||||
content={props.part.text.trim()}
|
content={props.part.text.trim()}
|
||||||
conceal={ctx.conceal()}
|
conceal={ctx.conceal()}
|
||||||
|
fg={theme.text}
|
||||||
/>
|
/>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -58,16 +58,14 @@ export const TuiThreadCommand = cmd({
|
|||||||
// Resolve relative paths against PWD to preserve behavior when using --cwd flag
|
// Resolve relative paths against PWD to preserve behavior when using --cwd flag
|
||||||
const baseCwd = process.env.PWD ?? process.cwd()
|
const baseCwd = process.env.PWD ?? process.cwd()
|
||||||
const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd()
|
const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd()
|
||||||
const defaultWorker = new URL("./worker.ts", import.meta.url)
|
const localWorker = new URL("./worker.ts", import.meta.url)
|
||||||
// Nix build creates a bundled worker next to the binary; prefer it when present.
|
const distWorker = new URL("./cli/cmd/tui/worker.js", import.meta.url)
|
||||||
const execDir = path.dirname(process.execPath)
|
const execDir = path.dirname(process.execPath)
|
||||||
const bundledWorker = path.join(execDir, "opencode-worker.js")
|
const workerPath = await iife(async () => {
|
||||||
const hasBundledWorker = await Bun.file(bundledWorker).exists()
|
|
||||||
const workerPath = (() => {
|
|
||||||
if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH
|
if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH
|
||||||
if (hasBundledWorker) return bundledWorker
|
if (await Bun.file(distWorker).exists()) return distWorker
|
||||||
return defaultWorker
|
return localWorker
|
||||||
})()
|
})
|
||||||
try {
|
try {
|
||||||
process.chdir(cwd)
|
process.chdir(cwd)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -99,7 +97,8 @@ export const TuiThreadCommand = cmd({
|
|||||||
if (!args.prompt) return piped
|
if (!args.prompt) return piped
|
||||||
return piped ? piped + "\n" + args.prompt : args.prompt
|
return piped ? piped + "\n" + args.prompt : args.prompt
|
||||||
})
|
})
|
||||||
await tui({
|
|
||||||
|
const tuiPromise = tui({
|
||||||
url: server.url,
|
url: server.url,
|
||||||
args: {
|
args: {
|
||||||
continue: args.continue,
|
continue: args.continue,
|
||||||
@@ -112,5 +111,11 @@ export const TuiThreadCommand = cmd({
|
|||||||
await client.call("shutdown", undefined)
|
await client.call("shutdown", undefined)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
client.call("checkUpgrade", { directory: cwd }).catch(() => {})
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
await tuiPromise
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function DialogAlert(props: DialogAlertProps) {
|
|||||||
dialog.clear()
|
dialog.clear()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<text fg={theme.background}>ok</text>
|
<text fg={theme.selectedListItemText}>ok</text>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
|
|||||||
@@ -53,7 +53,9 @@ export function DialogConfirm(props: DialogConfirmProps) {
|
|||||||
dialog.clear()
|
dialog.clear()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<text fg={key === store.active ? theme.background : theme.textMuted}>{Locale.titlecase(key)}</text>
|
<text fg={key === store.active ? theme.selectedListItemText : theme.textMuted}>
|
||||||
|
{Locale.titlecase(key)}
|
||||||
|
</text>
|
||||||
</box>
|
</box>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export function DialogHelp() {
|
|||||||
</box>
|
</box>
|
||||||
<box flexDirection="row" justifyContent="flex-end" paddingBottom={1}>
|
<box flexDirection="row" justifyContent="flex-end" paddingBottom={1}>
|
||||||
<box paddingLeft={3} paddingRight={3} backgroundColor={theme.primary} onMouseUp={() => dialog.clear()}>
|
<box paddingLeft={3} paddingRight={3} backgroundColor={theme.primary} onMouseUp={() => dialog.clear()}>
|
||||||
<text fg={theme.background}>ok</text>
|
<text fg={theme.selectedListItemText}>ok</text>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { InputRenderable, RGBA, ScrollBoxRenderable, TextAttributes } from "@opentui/core"
|
import { InputRenderable, RGBA, ScrollBoxRenderable, TextAttributes } from "@opentui/core"
|
||||||
import { useTheme } from "@tui/context/theme"
|
import { useTheme, selectedForeground } from "@tui/context/theme"
|
||||||
import { entries, filter, flatMap, groupBy, pipe, take } from "remeda"
|
import { entries, filter, flatMap, groupBy, pipe, take } from "remeda"
|
||||||
import { batch, createEffect, createMemo, For, Show, type JSX } from "solid-js"
|
import { batch, createEffect, createMemo, For, Show, type JSX } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
@@ -262,32 +262,29 @@ function Option(props: {
|
|||||||
onMouseOver?: () => void
|
onMouseOver?: () => void
|
||||||
}) {
|
}) {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
const fg = selectedForeground(theme)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Show when={props.current}>
|
<Show when={props.current}>
|
||||||
<text
|
<text flexShrink={0} fg={props.active ? fg : props.current ? theme.primary : theme.text} marginRight={0.5}>
|
||||||
flexShrink={0}
|
●
|
||||||
fg={props.active ? theme.background : props.current ? theme.primary : theme.text}
|
|
||||||
marginRight={0.5}
|
|
||||||
>
|
|
||||||
◆
|
|
||||||
</text>
|
</text>
|
||||||
</Show>
|
</Show>
|
||||||
<text
|
<text
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
fg={props.active ? theme.background : props.current ? theme.primary : theme.text}
|
fg={props.active ? fg : props.current ? theme.primary : theme.text}
|
||||||
attributes={props.active ? TextAttributes.BOLD : undefined}
|
attributes={props.active ? TextAttributes.BOLD : undefined}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
wrapMode="none"
|
wrapMode="none"
|
||||||
paddingLeft={3}
|
paddingLeft={3}
|
||||||
>
|
>
|
||||||
{Locale.truncate(props.title, 62)}
|
{Locale.truncate(props.title, 62)}
|
||||||
<span style={{ fg: props.active ? theme.background : theme.textMuted }}> {props.description}</span>
|
<span style={{ fg: props.active ? fg : theme.textMuted }}> {props.description}</span>
|
||||||
</text>
|
</text>
|
||||||
<Show when={props.footer}>
|
<Show when={props.footer}>
|
||||||
<box flexShrink={0}>
|
<box flexShrink={0}>
|
||||||
<text fg={props.active ? theme.background : theme.textMuted}>{props.footer}</text>
|
<text fg={props.active ? fg : theme.textMuted}>{props.footer}</text>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ process.on("uncaughtException", (e) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
upgrade()
|
|
||||||
|
|
||||||
let server: Bun.Server<undefined>
|
let server: Bun.Server<undefined>
|
||||||
export const rpc = {
|
export const rpc = {
|
||||||
async server(input: { port: number; hostname: string }) {
|
async server(input: { port: number; hostname: string }) {
|
||||||
@@ -42,6 +40,14 @@ export const rpc = {
|
|||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async checkUpgrade(input: { directory: string }) {
|
||||||
|
await Instance.provide({
|
||||||
|
directory: input.directory,
|
||||||
|
fn: async () => {
|
||||||
|
await upgrade().catch(() => {})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
async shutdown() {
|
async shutdown() {
|
||||||
Log.Default.info("worker shutting down")
|
Log.Default.info("worker shutting down")
|
||||||
await Instance.disposeAll()
|
await Instance.disposeAll()
|
||||||
|
|||||||
@@ -5,10 +5,15 @@ import { Installation } from "@/installation"
|
|||||||
|
|
||||||
export async function upgrade() {
|
export async function upgrade() {
|
||||||
const config = await Config.global()
|
const config = await Config.global()
|
||||||
if (config.autoupdate === false || Flag.OPENCODE_DISABLE_AUTOUPDATE) return
|
|
||||||
const latest = await Installation.latest().catch(() => {})
|
const latest = await Installation.latest().catch(() => {})
|
||||||
if (!latest) return
|
if (!latest) return
|
||||||
if (Installation.VERSION === latest) return
|
if (Installation.VERSION === latest) return
|
||||||
|
|
||||||
|
if (config.autoupdate === false || Flag.OPENCODE_DISABLE_AUTOUPDATE) {
|
||||||
|
await Bus.publish(Installation.Event.UpdateAvailable, { version: latest })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const method = await Installation.method()
|
const method = await Installation.method()
|
||||||
if (method === "unknown") return
|
if (method === "unknown") return
|
||||||
await Installation.upgrade(method, latest)
|
await Installation.upgrade(method, latest)
|
||||||
|
|||||||
@@ -430,6 +430,7 @@ export namespace Config {
|
|||||||
history_next: z.string().optional().default("down").describe("Next history item"),
|
history_next: z.string().optional().default("down").describe("Next history item"),
|
||||||
session_child_cycle: z.string().optional().default("<leader>right").describe("Next child session"),
|
session_child_cycle: z.string().optional().default("<leader>right").describe("Next child session"),
|
||||||
session_child_cycle_reverse: z.string().optional().default("<leader>left").describe("Previous child session"),
|
session_child_cycle_reverse: z.string().optional().default("<leader>left").describe("Previous child session"),
|
||||||
|
terminal_suspend: z.string().optional().default("ctrl+z").describe("Suspend terminal"),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.meta({
|
.meta({
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ await Promise.all([
|
|||||||
fs.mkdir(Global.Path.bin, { recursive: true }),
|
fs.mkdir(Global.Path.bin, { recursive: true }),
|
||||||
])
|
])
|
||||||
|
|
||||||
const CACHE_VERSION = "9"
|
const CACHE_VERSION = "11"
|
||||||
|
|
||||||
const version = await Bun.file(path.join(Global.Path.cache, "version"))
|
const version = await Bun.file(path.join(Global.Path.cache, "version"))
|
||||||
.text()
|
.text()
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ export namespace Installation {
|
|||||||
version: z.string(),
|
version: z.string(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
UpdateAvailable: Bus.event(
|
||||||
|
"installation.update-available",
|
||||||
|
z.object({
|
||||||
|
version: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Info = z
|
export const Info = z
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export namespace Plugin {
|
|||||||
}
|
}
|
||||||
const plugins = [...(config.plugin ?? [])]
|
const plugins = [...(config.plugin ?? [])]
|
||||||
if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
|
if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
|
||||||
plugins.push("opencode-copilot-auth@0.0.5")
|
plugins.push("opencode-copilot-auth@0.0.7")
|
||||||
plugins.push("opencode-anthropic-auth@0.0.2")
|
plugins.push("opencode-anthropic-auth@0.0.2")
|
||||||
}
|
}
|
||||||
for (let plugin of plugins) {
|
for (let plugin of plugins) {
|
||||||
|
|||||||
@@ -530,13 +530,18 @@ export namespace Provider {
|
|||||||
const existing = s.sdk.get(key)
|
const existing = s.sdk.get(key)
|
||||||
if (existing) return existing
|
if (existing) return existing
|
||||||
|
|
||||||
let installedPath: string
|
const installedPath = await (async () => {
|
||||||
if (!pkg.startsWith("file://")) {
|
if (pkg.startsWith("file://")) {
|
||||||
installedPath = await BunProc.install(pkg, "latest")
|
log.info("loading local provider", { pkg })
|
||||||
} else {
|
return pkg
|
||||||
log.info("loading local provider", { pkg })
|
}
|
||||||
installedPath = pkg
|
const resolved = await BunProc.resolve(pkg)
|
||||||
}
|
if (resolved) {
|
||||||
|
log.info("using preinstalled provider", { providerID: provider.id, pkg })
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
|
return BunProc.install(pkg, "latest")
|
||||||
|
})()
|
||||||
|
|
||||||
// The `google-vertex-anthropic` provider points to the `@ai-sdk/google-vertex` package.
|
// The `google-vertex-anthropic` provider points to the `@ai-sdk/google-vertex` package.
|
||||||
// Ref: https://github.com/sst/models.dev/blob/0a87de42ab177bebad0620a889e2eb2b4a5dd4ab/providers/google-vertex-anthropic/provider.toml
|
// Ref: https://github.com/sst/models.dev/blob/0a87de42ab177bebad0620a889e2eb2b4a5dd4ab/providers/google-vertex-anthropic/provider.toml
|
||||||
@@ -671,7 +676,7 @@ export namespace Provider {
|
|||||||
if (providerID === "github-copilot") {
|
if (providerID === "github-copilot") {
|
||||||
priority = priority.filter((m) => m !== "claude-haiku-4.5")
|
priority = priority.filter((m) => m !== "claude-haiku-4.5")
|
||||||
}
|
}
|
||||||
if (providerID === "opencode" || providerID === "local") {
|
if (providerID.startsWith("opencode")) {
|
||||||
priority = ["gpt-5-nano"]
|
priority = ["gpt-5-nano"]
|
||||||
}
|
}
|
||||||
for (const item of priority) {
|
for (const item of priority) {
|
||||||
@@ -680,7 +685,14 @@ export namespace Provider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return getModel("opencode", "gpt-5-nano")
|
|
||||||
|
// Check if opencode provider is available before using it
|
||||||
|
const opencodeProvider = await state().then((state) => state.providers["opencode"])
|
||||||
|
if (opencodeProvider && opencodeProvider.info.models["gpt-5-nano"]) {
|
||||||
|
return getModel("opencode", "gpt-5-nano")
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
|
const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
|
||||||
|
|||||||
@@ -272,4 +272,12 @@ export namespace ProviderTransform {
|
|||||||
|
|
||||||
return schema
|
return schema
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function error(providerID: string, message: string) {
|
||||||
|
if (providerID === "github-copilot" && message.includes("The requested model is not supported")) {
|
||||||
|
message +=
|
||||||
|
"\n\nMake sure the model is enabled in your copilot settings: https://github.com/settings/copilot/features"
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { LSP } from "../lsp"
|
|||||||
import { Snapshot } from "@/snapshot"
|
import { Snapshot } from "@/snapshot"
|
||||||
import { fn } from "@/util/fn"
|
import { fn } from "@/util/fn"
|
||||||
import { Storage } from "@/storage/storage"
|
import { Storage } from "@/storage/storage"
|
||||||
|
import { ProviderTransform } from "@/provider/transform"
|
||||||
|
|
||||||
export namespace MessageV2 {
|
export namespace MessageV2 {
|
||||||
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
|
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
|
||||||
@@ -737,9 +738,10 @@ export namespace MessageV2 {
|
|||||||
{ cause: e },
|
{ cause: e },
|
||||||
).toObject()
|
).toObject()
|
||||||
case APICallError.isInstance(e):
|
case APICallError.isInstance(e):
|
||||||
|
const message = ProviderTransform.error(ctx.providerID, e.message)
|
||||||
return new MessageV2.APIError(
|
return new MessageV2.APIError(
|
||||||
{
|
{
|
||||||
message: e.message,
|
message,
|
||||||
statusCode: e.statusCode,
|
statusCode: e.statusCode,
|
||||||
isRetryable: e.isRetryable,
|
isRetryable: e.isRetryable,
|
||||||
responseHeaders: e.responseHeaders,
|
responseHeaders: e.responseHeaders,
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ export namespace SessionPrompt {
|
|||||||
const message = await createUserMessage(input)
|
const message = await createUserMessage(input)
|
||||||
await Session.touch(input.sessionID)
|
await Session.touch(input.sessionID)
|
||||||
|
|
||||||
if (input.noReply) {
|
if (input.noReply === true) {
|
||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,7 +534,7 @@ export namespace SessionPrompt {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
headers: {
|
headers: {
|
||||||
...(model.providerID === "opencode"
|
...(model.providerID.startsWith("opencode")
|
||||||
? {
|
? {
|
||||||
"x-opencode-session": sessionID,
|
"x-opencode-session": sessionID,
|
||||||
"x-opencode-request": lastUser.id,
|
"x-opencode-request": lastUser.id,
|
||||||
@@ -907,22 +907,32 @@ export namespace SessionPrompt {
|
|||||||
extra: { bypassCwdCheck: true, ...info.model },
|
extra: { bypassCwdCheck: true, ...info.model },
|
||||||
metadata: async () => {},
|
metadata: async () => {},
|
||||||
})
|
})
|
||||||
pieces.push(
|
pieces.push({
|
||||||
{
|
id: Identifier.ascending("part"),
|
||||||
id: Identifier.ascending("part"),
|
messageID: info.id,
|
||||||
messageID: info.id,
|
sessionID: input.sessionID,
|
||||||
sessionID: input.sessionID,
|
type: "text",
|
||||||
type: "text",
|
synthetic: true,
|
||||||
synthetic: true,
|
text: result.output,
|
||||||
text: result.output,
|
})
|
||||||
},
|
if (result.attachments?.length) {
|
||||||
{
|
pieces.push(
|
||||||
|
...result.attachments.map((attachment) => ({
|
||||||
|
...attachment,
|
||||||
|
synthetic: true,
|
||||||
|
filename: attachment.filename ?? part.filename,
|
||||||
|
messageID: info.id,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
pieces.push({
|
||||||
...part,
|
...part,
|
||||||
id: part.id ?? Identifier.ascending("part"),
|
id: part.id ?? Identifier.ascending("part"),
|
||||||
messageID: info.id,
|
messageID: info.id,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
},
|
})
|
||||||
)
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
log.error("failed to read file", { error })
|
log.error("failed to read file", { error })
|
||||||
@@ -1398,7 +1408,8 @@ export namespace SessionPrompt {
|
|||||||
input.history.filter((m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic))
|
input.history.filter((m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic))
|
||||||
.length === 1
|
.length === 1
|
||||||
if (!isFirst) return
|
if (!isFirst) return
|
||||||
const small = await Provider.getSmallModel(input.providerID)
|
const small =
|
||||||
|
(await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID))
|
||||||
const options = pipe(
|
const options = pipe(
|
||||||
{},
|
{},
|
||||||
mergeDeep(ProviderTransform.options(small.providerID, small.modelID, small.npm ?? "", input.session.id)),
|
mergeDeep(ProviderTransform.options(small.providerID, small.modelID, small.npm ?? "", input.session.id)),
|
||||||
@@ -1406,7 +1417,8 @@ export namespace SessionPrompt {
|
|||||||
mergeDeep(small.info.options),
|
mergeDeep(small.info.options),
|
||||||
)
|
)
|
||||||
await generateText({
|
await generateText({
|
||||||
maxOutputTokens: small.info.reasoning ? 1500 : 20,
|
// use higher # for reasoning models since reasoning tokens eat up a lot of the budget
|
||||||
|
maxOutputTokens: small.info.reasoning ? 3000 : 20,
|
||||||
providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, options),
|
providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, options),
|
||||||
messages: [
|
messages: [
|
||||||
...SystemPrompt.title(small.providerID).map(
|
...SystemPrompt.title(small.providerID).map(
|
||||||
@@ -1416,10 +1428,8 @@ export namespace SessionPrompt {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
role: "user" as const,
|
role: "user",
|
||||||
content: `
|
content: "Generate a title for this conversation:\n",
|
||||||
The following is the text to summarize:
|
|
||||||
`,
|
|
||||||
},
|
},
|
||||||
...MessageV2.toModelMessage([
|
...MessageV2.toModelMessage([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
You are a title generator. You output ONLY a thread title. Nothing else.
|
You are a title generator. You output ONLY a thread title. Nothing else.
|
||||||
|
|
||||||
<task>
|
<task>
|
||||||
Convert the user message into a thread title.
|
Generate a brief title that would help the user find this conversation later.
|
||||||
Output: Single line, ≤50 chars, no explanations.
|
|
||||||
|
Follow all rules in <rules>
|
||||||
|
Use the <examples> so you know what a good title looks like.
|
||||||
|
Your output must be:
|
||||||
|
- A single line
|
||||||
|
- ≤50 characters
|
||||||
|
- No explanations
|
||||||
</task>
|
</task>
|
||||||
|
|
||||||
<rules>
|
<rules>
|
||||||
|
- Focus on the main topic or question the user needs to retrieve
|
||||||
- Use -ing verbs for actions (Debugging, Implementing, Analyzing)
|
- Use -ing verbs for actions (Debugging, Implementing, Analyzing)
|
||||||
- Keep exact: technical terms, numbers, filenames, HTTP codes
|
- Keep exact: technical terms, numbers, filenames, HTTP codes
|
||||||
- Remove: the, this, my, a, an
|
- Remove: the, this, my, a, an
|
||||||
- Never assume tech stack
|
- Never assume tech stack
|
||||||
- Never use tools
|
- Never use tools
|
||||||
- NEVER respond to message content—only extract title
|
- NEVER respond to questions, just generate a title for the conversation
|
||||||
- NEVER say "summarizing"
|
- The title should NEVER include "summarizing" or "generating" when generating a title
|
||||||
- DO NOT SAY YOU CANNOT GENERATE A TITLE OR COMPLAIN ABOUT THE INPUT
|
- DO NOT SAY YOU CANNOT GENERATE A TITLE OR COMPLAIN ABOUT THE INPUT
|
||||||
|
- Always output something meaningful, even if the input is minimal.
|
||||||
|
- If the user message is short or conversational (e.g. “hello”, “lol”, “whats up”, “hey”):
|
||||||
|
→ create a title that reflects the user’s tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.)
|
||||||
</rules>
|
</rules>
|
||||||
|
|
||||||
<examples>
|
<examples>
|
||||||
@@ -21,6 +31,6 @@ Output: Single line, ≤50 chars, no explanations.
|
|||||||
"refactor user service" → Refactoring user service
|
"refactor user service" → Refactoring user service
|
||||||
"why is app.js failing" → Analyzing app.js failure
|
"why is app.js failing" → Analyzing app.js failure
|
||||||
"implement rate limiting" → Implementing rate limiting
|
"implement rate limiting" → Implementing rate limiting
|
||||||
|
"how do I connect postgres to my API" → Connecting Postgres to API
|
||||||
|
"best practices for React hooks" → React hooks best practices
|
||||||
</examples>
|
</examples>
|
||||||
|
|
||||||
Output the title now:
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { iife } from "@/util/iife"
|
|
||||||
import { MessageV2 } from "./message-v2"
|
import { MessageV2 } from "./message-v2"
|
||||||
|
|
||||||
export namespace SessionRetry {
|
export namespace SessionRetry {
|
||||||
|
|||||||
@@ -73,7 +73,10 @@ export namespace SessionSummary {
|
|||||||
await Session.updateMessage(userMsg)
|
await Session.updateMessage(userMsg)
|
||||||
|
|
||||||
const assistantMsg = messages.find((m) => m.info.role === "assistant")!.info as MessageV2.Assistant
|
const assistantMsg = messages.find((m) => m.info.role === "assistant")!.info as MessageV2.Assistant
|
||||||
const small = await Provider.getSmallModel(assistantMsg.providerID)
|
const small =
|
||||||
|
(await Provider.getSmallModel(assistantMsg.providerID)) ??
|
||||||
|
(await Provider.getModel(assistantMsg.providerID, assistantMsg.modelID))
|
||||||
|
|
||||||
const options = pipe(
|
const options = pipe(
|
||||||
{},
|
{},
|
||||||
mergeDeep(ProviderTransform.options(small.providerID, small.modelID, small.npm ?? "", assistantMsg.sessionID)),
|
mergeDeep(ProviderTransform.options(small.providerID, small.modelID, small.npm ?? "", assistantMsg.sessionID)),
|
||||||
|
|||||||
@@ -70,6 +70,12 @@ export const BashTool = Tool.define("bash", async () => {
|
|||||||
return "/bin/zsh"
|
return "/bin/zsh"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
// Let Bun / Node pick COMSPEC (usually cmd.exe)
|
||||||
|
// or explicitly:
|
||||||
|
return process.env.COMSPEC || true
|
||||||
|
}
|
||||||
|
|
||||||
const bash = Bun.which("bash")
|
const bash = Bun.which("bash")
|
||||||
if (bash) {
|
if (bash) {
|
||||||
return bash
|
return bash
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.0.102",
|
"version": "1.0.107",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.0.102",
|
"version": "1.0.107",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ export type EventInstallationUpdated = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EventInstallationUpdateAvailable = {
|
||||||
|
type: "installation.update-available"
|
||||||
|
properties: {
|
||||||
|
version: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type EventLspClientDiagnostics = {
|
export type EventLspClientDiagnostics = {
|
||||||
type: "lsp.client.diagnostics"
|
type: "lsp.client.diagnostics"
|
||||||
properties: {
|
properties: {
|
||||||
@@ -642,6 +649,7 @@ export type EventFileWatcherUpdated = {
|
|||||||
|
|
||||||
export type Event =
|
export type Event =
|
||||||
| EventInstallationUpdated
|
| EventInstallationUpdated
|
||||||
|
| EventInstallationUpdateAvailable
|
||||||
| EventLspClientDiagnostics
|
| EventLspClientDiagnostics
|
||||||
| EventLspUpdated
|
| EventLspUpdated
|
||||||
| EventMessageUpdated
|
| EventMessageUpdated
|
||||||
@@ -846,6 +854,10 @@ export type KeybindsConfig = {
|
|||||||
* Previous child session
|
* Previous child session
|
||||||
*/
|
*/
|
||||||
session_child_cycle_reverse?: string
|
session_child_cycle_reverse?: string
|
||||||
|
/**
|
||||||
|
* Suspend terminal
|
||||||
|
*/
|
||||||
|
terminal_suspend?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AgentConfig = {
|
export type AgentConfig = {
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ class Resource:
|
|||||||
class Console:
|
class Console:
|
||||||
type: str
|
type: str
|
||||||
url: str
|
url: str
|
||||||
|
class ConsoleData:
|
||||||
|
name: str
|
||||||
|
type: str
|
||||||
class Database:
|
class Database:
|
||||||
database: str
|
database: str
|
||||||
host: str
|
host: str
|
||||||
@@ -52,6 +55,9 @@ class Resource:
|
|||||||
class EMAILOCTOPUS_API_KEY:
|
class EMAILOCTOPUS_API_KEY:
|
||||||
type: str
|
type: str
|
||||||
value: str
|
value: str
|
||||||
|
class EnterpriseStorage:
|
||||||
|
name: str
|
||||||
|
type: str
|
||||||
class GITHUB_APP_ID:
|
class GITHUB_APP_ID:
|
||||||
type: str
|
type: str
|
||||||
value: str
|
value: str
|
||||||
@@ -75,6 +81,12 @@ class Resource:
|
|||||||
value: str
|
value: str
|
||||||
class LogProcessor:
|
class LogProcessor:
|
||||||
type: str
|
type: str
|
||||||
|
class R2AccessKey:
|
||||||
|
type: str
|
||||||
|
value: str
|
||||||
|
class R2SecretKey:
|
||||||
|
type: str
|
||||||
|
value: str
|
||||||
class STRIPE_SECRET_KEY:
|
class STRIPE_SECRET_KEY:
|
||||||
type: str
|
type: str
|
||||||
value: str
|
value: str
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.0.102",
|
"version": "1.0.107",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run src/index.ts",
|
"dev": "bun run src/index.ts",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/tauri",
|
"name": "@opencode-ai/tauri",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.102",
|
"version": "1.0.107",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
{
|
{
|
||||||
"title": "opencode",
|
"title": "opencode",
|
||||||
"width": 800,
|
"width": 800,
|
||||||
"height": 600
|
"height": 600,
|
||||||
|
"url": "https://desktop.dev.opencode.ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
|
|||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
/* This file is auto-generated by SST. Do not edit. */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/* deno-fmt-ignore-file */
|
||||||
|
|
||||||
|
/// <reference path="../../sst-env.d.ts" />
|
||||||
|
|
||||||
|
import "sst"
|
||||||
|
export {}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.0.102",
|
"version": "1.0.107",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
"./*": "./src/components/*.tsx",
|
"./*": "./src/components/*.tsx",
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
[data-component="message-nav"] {
|
||||||
|
/* margin-right: 32px; */
|
||||||
|
/* margin-top: 12px; */
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding-left: 0;
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
&[data-size="normal"] {
|
||||||
|
position: absolute;
|
||||||
|
right: 100%;
|
||||||
|
width: 240px;
|
||||||
|
/* margin-top: 12px; */
|
||||||
|
|
||||||
|
@media (min-width: 80rem) {
|
||||||
|
gap: 8px;
|
||||||
|
/* margin-top: 4px; */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="message-nav-item"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
align-self: stretch;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
[data-component="message-nav"][data-size="normal"] & {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="message-nav-tick-button"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
height: 8px;
|
||||||
|
width: 32px;
|
||||||
|
/* margin-right: -12px; */
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&[data-active] [data-slot="message-nav-tick-line"] {
|
||||||
|
background-color: var(--icon-strong-base);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="message-nav-tick-line"] {
|
||||||
|
height: 1px;
|
||||||
|
width: 20px;
|
||||||
|
background-color: var(--icon-base);
|
||||||
|
transition:
|
||||||
|
width 0.2s,
|
||||||
|
background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="message-nav-tick-button"]:hover [data-slot="message-nav-tick-line"] {
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--icon-strong-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="message-nav-message-button"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
align-self: stretch;
|
||||||
|
width: 100%;
|
||||||
|
column-gap: 8px;
|
||||||
|
cursor: default;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="message-nav-title-preview"] {
|
||||||
|
font-size: 14px; /* text-14-regular */
|
||||||
|
color: var(--text-weak);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-width: 0;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
&[data-active] {
|
||||||
|
color: var(--text-strong);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="message-nav-item"]:hover [data-slot="message-nav-title-preview"] {
|
||||||
|
color: var(--text-base);
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { UserMessage } from "@opencode-ai/sdk"
|
||||||
|
import { ComponentProps, createMemo, For, Match, Show, splitProps, Switch } from "solid-js"
|
||||||
|
import { DiffChanges } from "./diff-changes"
|
||||||
|
import { Spinner } from "./spinner"
|
||||||
|
|
||||||
|
export function MessageNav(
|
||||||
|
props: ComponentProps<"ul"> & {
|
||||||
|
messages: UserMessage[]
|
||||||
|
current?: UserMessage
|
||||||
|
size: "normal" | "compact"
|
||||||
|
working?: boolean
|
||||||
|
onMessageSelect: (message: UserMessage) => void
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const [local, others] = splitProps(props, ["messages", "current", "size", "working", "onMessageSelect"])
|
||||||
|
const lastUserMessage = createMemo(() => {
|
||||||
|
return local.messages?.at(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul role="list" data-component="message-nav" data-size={local.size} {...others}>
|
||||||
|
<For each={local.messages}>
|
||||||
|
{(message) => {
|
||||||
|
const messageWorking = createMemo(() => message.id === lastUserMessage()?.id && local.working)
|
||||||
|
const handleClick = () => local.onMessageSelect(message)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li data-slot="message-nav-item">
|
||||||
|
<Switch>
|
||||||
|
<Match when={local.size === "compact"}>
|
||||||
|
<button
|
||||||
|
data-slot="message-nav-tick-button"
|
||||||
|
data-active={message.id === local.current?.id || undefined}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<div data-slot="message-nav-tick-line" />
|
||||||
|
</button>
|
||||||
|
</Match>
|
||||||
|
<Match when={local.size === "normal"}>
|
||||||
|
<button data-slot="message-nav-message-button" onClick={handleClick}>
|
||||||
|
<Switch>
|
||||||
|
<Match when={messageWorking()}>
|
||||||
|
<Spinner />
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<DiffChanges changes={message.summary?.diffs ?? []} variant="bars" />
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
<div
|
||||||
|
data-slot="message-nav-title-preview"
|
||||||
|
data-active={message.id === local.current?.id || undefined}
|
||||||
|
>
|
||||||
|
<Show when={message.summary?.title} fallback="New message">
|
||||||
|
{message.summary?.title}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ import { Part } from "./message-part"
|
|||||||
import { Spinner } from "./spinner"
|
import { Spinner } from "./spinner"
|
||||||
import { useData } from "../context/data"
|
import { useData } from "../context/data"
|
||||||
import type { AssistantMessage as AssistantMessageType, ToolPart } from "@opencode-ai/sdk"
|
import type { AssistantMessage as AssistantMessageType, ToolPart } from "@opencode-ai/sdk"
|
||||||
import "./message-progress.css"
|
|
||||||
|
|
||||||
export function MessageProgress(props: { assistantMessages: () => AssistantMessageType[]; done?: boolean }) {
|
export function MessageProgress(props: { assistantMessages: () => AssistantMessageType[]; done?: boolean }) {
|
||||||
const data = useData()
|
const data = useData()
|
||||||
|
|||||||
@@ -1,324 +0,0 @@
|
|||||||
[data-component="session-timeline"] {
|
|
||||||
/* flex: 1; */
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: flex-start;
|
|
||||||
|
|
||||||
[data-slot="session-timeline-timeline-list"] {
|
|
||||||
margin-right: 32px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-top: 12px;
|
|
||||||
|
|
||||||
&[data-expanded="true"] {
|
|
||||||
position: absolute;
|
|
||||||
right: 100%;
|
|
||||||
width: 240px;
|
|
||||||
margin-top: 12px;
|
|
||||||
|
|
||||||
@media (min-width: 80rem) {
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-timeline-item"] {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
align-self: stretch;
|
|
||||||
justify-content: flex-end;
|
|
||||||
|
|
||||||
&[data-expanded="true"] {
|
|
||||||
@media (min-width: 80rem) {
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-tick-button"] {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
height: 8px;
|
|
||||||
width: 32px;
|
|
||||||
margin-right: -12px;
|
|
||||||
cursor: pointer;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
&[data-active="true"] [data-slot="session-timeline-tick-line"] {
|
|
||||||
background-color: var(--icon-strong-base);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-expanded="true"] {
|
|
||||||
@media (min-width: 80rem) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-tick-line"] {
|
|
||||||
height: 1px;
|
|
||||||
width: 20px;
|
|
||||||
background-color: var(--icon-base);
|
|
||||||
transition:
|
|
||||||
width 0.2s,
|
|
||||||
background-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-tick-button"]:hover [data-slot="session-timeline-tick-line"] {
|
|
||||||
width: 100%;
|
|
||||||
background-color: var(--icon-strong-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-message-button"] {
|
|
||||||
display: none;
|
|
||||||
align-items: center;
|
|
||||||
align-self: stretch;
|
|
||||||
width: 100%;
|
|
||||||
column-gap: 8px;
|
|
||||||
cursor: default;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
&[data-expanded="true"] {
|
|
||||||
@media (min-width: 80rem) {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-message-title-preview"] {
|
|
||||||
font-size: 14px; /* text-14-regular */
|
|
||||||
color: var(--text-weak);
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
min-width: 0;
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
&[data-active="true"] {
|
|
||||||
color: var(--text-strong);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-timeline-item"]:hover [data-slot="session-timeline-message-title-preview"] {
|
|
||||||
color: var(--text-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-content"] {
|
|
||||||
flex-grow: 1;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-content"]::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-message-container"] {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
align-self: stretch;
|
|
||||||
gap: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-message-header"] {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
align-self: stretch;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
background-color: var(--background-stronger);
|
|
||||||
z-index: 20;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-message-content"] {
|
|
||||||
margin-top: -24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-message-title"] {
|
|
||||||
width: 100%;
|
|
||||||
font-size: 14px; /* text-14-medium */
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-strong);
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
min-width: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-message-title"] h1 {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
min-width: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-size: inherit;
|
|
||||||
font-weight: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-typewriter"] {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
min-width: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-summary-section"] {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 24px;
|
|
||||||
align-items: flex-start;
|
|
||||||
align-self: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-summary-header"] {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 4px;
|
|
||||||
align-self: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-summary-title"] {
|
|
||||||
font-size: 12px; /* text-12-medium */
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-weak);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-markdown"] {
|
|
||||||
&[data-diffs="true"] {
|
|
||||||
font-size: 14px; /* text-14-regular */
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-fade="true"] > * {
|
|
||||||
animation: fade-up-text 0.3s ease-out forwards;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-accordion"] {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="sticky-accordion-header"] {
|
|
||||||
top: 40px;
|
|
||||||
|
|
||||||
&[data-expanded]::before {
|
|
||||||
top: -40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-accordion-trigger-content"] {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-file-info"] {
|
|
||||||
flex-grow: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 20px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-file-icon"] {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-file-path"] {
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-directory"] {
|
|
||||||
color: var(--text-base);
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
direction: rtl;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-filename"] {
|
|
||||||
color: var(--text-strong);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-accordion-actions"] {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-accordion-content"] {
|
|
||||||
max-height: 240px; /* max-h-60 */
|
|
||||||
overflow-y: auto;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-accordion-content"]::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-response-section"] {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-collapsible-trigger-content"] {
|
|
||||||
color: var(--text-weak);
|
|
||||||
cursor: pointer;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--text-strong);
|
|
||||||
}
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
align-self: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-details-text"] {
|
|
||||||
font-size: 12px; /* text-12-medium */
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-card {
|
|
||||||
color: var(--text-on-critical-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="session-timeline-collapsible-content-inner"] {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
align-self: stretch;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,289 +0,0 @@
|
|||||||
import { AssistantMessage } from "@opencode-ai/sdk"
|
|
||||||
import { useData } from "../context"
|
|
||||||
import { Binary } from "@opencode-ai/util/binary"
|
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
|
||||||
import { createEffect, createMemo, createSignal, For, Match, ParentProps, Show, Switch } from "solid-js"
|
|
||||||
import { createStore } from "solid-js/store"
|
|
||||||
import { DiffChanges } from "./diff-changes"
|
|
||||||
import { Spinner } from "./spinner"
|
|
||||||
import { Typewriter } from "./typewriter"
|
|
||||||
import { Message } from "./message-part"
|
|
||||||
import { Markdown } from "./markdown"
|
|
||||||
import { Accordion } from "./accordion"
|
|
||||||
import { StickyAccordionHeader } from "./sticky-accordion-header"
|
|
||||||
import { FileIcon } from "./file-icon"
|
|
||||||
import { Icon } from "./icon"
|
|
||||||
import { Diff } from "./diff"
|
|
||||||
import { Card } from "./card"
|
|
||||||
import { MessageProgress } from "./message-progress"
|
|
||||||
import { Collapsible } from "./collapsible"
|
|
||||||
|
|
||||||
export function SessionTimeline(
|
|
||||||
props: ParentProps<{
|
|
||||||
sessionID: string
|
|
||||||
classes?: {
|
|
||||||
root?: string
|
|
||||||
content?: string
|
|
||||||
container?: string
|
|
||||||
}
|
|
||||||
expanded?: boolean
|
|
||||||
}>,
|
|
||||||
) {
|
|
||||||
const data = useData()
|
|
||||||
const [store, setStore] = createStore({
|
|
||||||
messageId: undefined as string | undefined,
|
|
||||||
})
|
|
||||||
const match = Binary.search(data.session, props.sessionID, (s) => s.id)
|
|
||||||
if (!match.found) throw new Error(`Session ${props.sessionID} not found`)
|
|
||||||
|
|
||||||
// const info = createMemo(() => data.session[match.index])
|
|
||||||
const messages = createMemo(() => (props.sessionID ? (data.message[props.sessionID] ?? []) : []))
|
|
||||||
const userMessages = createMemo(() =>
|
|
||||||
messages()
|
|
||||||
.filter((m) => m.role === "user")
|
|
||||||
.sort((a, b) => b.id.localeCompare(a.id)),
|
|
||||||
)
|
|
||||||
const lastUserMessage = createMemo(() => {
|
|
||||||
return userMessages()?.at(0)
|
|
||||||
})
|
|
||||||
const activeMessage = createMemo(() => {
|
|
||||||
if (!store.messageId) return lastUserMessage()
|
|
||||||
return userMessages()?.find((m) => m.id === store.messageId)
|
|
||||||
})
|
|
||||||
const status = createMemo(
|
|
||||||
() =>
|
|
||||||
data.session_status[props.sessionID] ?? {
|
|
||||||
type: "idle",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
const working = createMemo(() => status()?.type !== "idle")
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div data-component="session-timeline" class={props.classes?.root}>
|
|
||||||
<Show when={userMessages().length > 1}>
|
|
||||||
<ul role="list" data-slot="session-timeline-timeline-list" data-expanded={props.expanded}>
|
|
||||||
<For each={userMessages()}>
|
|
||||||
{(message) => {
|
|
||||||
const messageWorking = createMemo(() => message.id === lastUserMessage()?.id && working())
|
|
||||||
const handleClick = () => setStore("messageId", message.id)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li data-slot="session-timeline-timeline-item" data-expanded={props.expanded}>
|
|
||||||
<button
|
|
||||||
data-slot="session-timeline-tick-button"
|
|
||||||
data-active={activeMessage()?.id === message.id}
|
|
||||||
data-expanded={props.expanded}
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
<div data-slot="session-timeline-tick-line" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
data-slot="session-timeline-message-button"
|
|
||||||
data-expanded={props.expanded}
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
<Switch>
|
|
||||||
<Match when={messageWorking()}>
|
|
||||||
<Spinner />
|
|
||||||
</Match>
|
|
||||||
<Match when={true}>
|
|
||||||
<DiffChanges changes={message.summary?.diffs ?? []} variant="bars" />
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
<div
|
|
||||||
data-slot="session-timeline-message-title-preview"
|
|
||||||
data-active={activeMessage()?.id === message.id}
|
|
||||||
>
|
|
||||||
<Show when={message.summary?.title} fallback="New message">
|
|
||||||
{message.summary?.title}
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</ul>
|
|
||||||
</Show>
|
|
||||||
<div data-slot="session-timeline-content" class={props.classes?.content}>
|
|
||||||
<For each={userMessages()}>
|
|
||||||
{(message) => {
|
|
||||||
const isActive = createMemo(() => activeMessage()?.id === message.id)
|
|
||||||
const titleSeen = createMemo(() => true)
|
|
||||||
const contentSeen = createMemo(() => true)
|
|
||||||
{
|
|
||||||
/* const titleSeen = createSeen(`message-title-${message.id}`) */
|
|
||||||
}
|
|
||||||
{
|
|
||||||
/* const contentSeen = createSeen(`message-content-${message.id}`) */
|
|
||||||
}
|
|
||||||
const [titled, setTitled] = createSignal(titleSeen())
|
|
||||||
const assistantMessages = createMemo(() => {
|
|
||||||
return messages()?.filter((m) => m.role === "assistant" && m.parentID == message.id) as AssistantMessage[]
|
|
||||||
})
|
|
||||||
const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
|
|
||||||
const [detailsExpanded, setDetailsExpanded] = createSignal(false)
|
|
||||||
const parts = createMemo(() => data.part[message.id])
|
|
||||||
const hasToolPart = createMemo(() =>
|
|
||||||
assistantMessages()
|
|
||||||
?.flatMap((m) => data.part[m.id])
|
|
||||||
.some((p) => p?.type === "tool"),
|
|
||||||
)
|
|
||||||
const messageWorking = createMemo(() => message.id === lastUserMessage()?.id && working())
|
|
||||||
const initialCompleted = !(message.id === lastUserMessage()?.id && working())
|
|
||||||
const [completed, setCompleted] = createSignal(initialCompleted)
|
|
||||||
|
|
||||||
// allowing time for the animations to finish
|
|
||||||
createEffect(() => {
|
|
||||||
if (titleSeen()) return
|
|
||||||
const title = message.summary?.title
|
|
||||||
if (title) setTimeout(() => setTitled(true), 10_000)
|
|
||||||
})
|
|
||||||
createEffect(() => {
|
|
||||||
const completed = !messageWorking()
|
|
||||||
setTimeout(() => setCompleted(completed), 1200)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Show when={isActive()}>
|
|
||||||
<div
|
|
||||||
data-message={message.id}
|
|
||||||
data-slot="session-timeline-message-container"
|
|
||||||
class={props.classes?.container}
|
|
||||||
>
|
|
||||||
{/* Title */}
|
|
||||||
<div data-slot="session-timeline-message-header">
|
|
||||||
<div data-slot="session-timeline-message-title">
|
|
||||||
<Show
|
|
||||||
when={titled()}
|
|
||||||
fallback={
|
|
||||||
<Typewriter as="h1" text={message.summary?.title} data-slot="session-timeline-typewriter" />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<h1>{message.summary?.title}</h1>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div data-slot="session-timeline-message-content">
|
|
||||||
<Message message={message} parts={parts()} />
|
|
||||||
</div>
|
|
||||||
{/* Summary */}
|
|
||||||
<Show when={completed()}>
|
|
||||||
<div data-slot="session-timeline-summary-section">
|
|
||||||
<div data-slot="session-timeline-summary-header">
|
|
||||||
<h2 data-slot="session-timeline-summary-title">
|
|
||||||
<Switch>
|
|
||||||
<Match when={message.summary?.diffs?.length}>Summary</Match>
|
|
||||||
<Match when={true}>Response</Match>
|
|
||||||
</Switch>
|
|
||||||
</h2>
|
|
||||||
<Show when={message.summary?.body}>
|
|
||||||
{(summary) => (
|
|
||||||
<Markdown
|
|
||||||
data-slot="session-timeline-markdown"
|
|
||||||
data-diffs={!!message.summary?.diffs?.length}
|
|
||||||
data-fade={!message.summary?.diffs?.length && !contentSeen()}
|
|
||||||
text={summary()}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<Accordion data-slot="session-timeline-accordion" multiple>
|
|
||||||
<For each={message.summary?.diffs ?? []}>
|
|
||||||
{(diff) => (
|
|
||||||
<Accordion.Item value={diff.file}>
|
|
||||||
<StickyAccordionHeader>
|
|
||||||
<Accordion.Trigger>
|
|
||||||
<div data-slot="session-timeline-accordion-trigger-content">
|
|
||||||
<div data-slot="session-timeline-file-info">
|
|
||||||
<FileIcon
|
|
||||||
node={{ path: diff.file, type: "file" }}
|
|
||||||
data-slot="session-timeline-file-icon"
|
|
||||||
/>
|
|
||||||
<div data-slot="session-timeline-file-path">
|
|
||||||
<Show when={diff.file.includes("/")}>
|
|
||||||
<span data-slot="session-timeline-directory">
|
|
||||||
{getDirectory(diff.file)}‎
|
|
||||||
</span>
|
|
||||||
</Show>
|
|
||||||
<span data-slot="session-timeline-filename">{getFilename(diff.file)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div data-slot="session-timeline-accordion-actions">
|
|
||||||
<DiffChanges changes={diff} />
|
|
||||||
<Icon name="chevron-grabber-vertical" size="small" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Accordion.Trigger>
|
|
||||||
</StickyAccordionHeader>
|
|
||||||
<Accordion.Content data-slot="session-timeline-accordion-content">
|
|
||||||
<Diff
|
|
||||||
before={{
|
|
||||||
name: diff.file!,
|
|
||||||
contents: diff.before!,
|
|
||||||
}}
|
|
||||||
after={{
|
|
||||||
name: diff.file!,
|
|
||||||
contents: diff.after!,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Accordion.Content>
|
|
||||||
</Accordion.Item>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</Accordion>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Show when={error() && !detailsExpanded()}>
|
|
||||||
<Card variant="error" class="error-card">
|
|
||||||
{error()?.data?.message as string}
|
|
||||||
</Card>
|
|
||||||
</Show>
|
|
||||||
{/* Response */}
|
|
||||||
<div data-slot="session-timeline-response-section">
|
|
||||||
<Switch>
|
|
||||||
<Match when={!completed()}>
|
|
||||||
<MessageProgress assistantMessages={assistantMessages} done={!messageWorking()} />
|
|
||||||
</Match>
|
|
||||||
<Match when={completed() && hasToolPart()}>
|
|
||||||
<Collapsible variant="ghost" open={detailsExpanded()} onOpenChange={setDetailsExpanded}>
|
|
||||||
<Collapsible.Trigger>
|
|
||||||
<div data-slot="session-timeline-collapsible-trigger-content">
|
|
||||||
<div data-slot="session-timeline-details-text">
|
|
||||||
<Switch>
|
|
||||||
<Match when={detailsExpanded()}>Hide details</Match>
|
|
||||||
<Match when={!detailsExpanded()}>Show details</Match>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
<Collapsible.Arrow />
|
|
||||||
</div>
|
|
||||||
</Collapsible.Trigger>
|
|
||||||
<Collapsible.Content>
|
|
||||||
<div data-slot="session-timeline-collapsible-content-inner">
|
|
||||||
<For each={assistantMessages()}>
|
|
||||||
{(assistantMessage) => {
|
|
||||||
const parts = createMemo(() => data.part[assistantMessage.id])
|
|
||||||
return <Message message={assistantMessage} parts={parts()} />
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
<Show when={error()}>
|
|
||||||
<Card variant="error" class="error-card">
|
|
||||||
{error()?.data?.message as string}
|
|
||||||
</Card>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Collapsible.Content>
|
|
||||||
</Collapsible>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
[data-component="session-turn"] {
|
||||||
|
/* flex: 1; */
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
[data-slot="session-turn-content"] {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-content"]::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-message-container"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
align-self: stretch;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-message-header"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
align-self: stretch;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background-color: var(--background-stronger);
|
||||||
|
z-index: 20;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-message-content"] {
|
||||||
|
margin-top: -24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-message-title"] {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 14px; /* text-14-medium */
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-strong);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-message-title"] h1 {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-typewriter"] {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-summary-section"] {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
align-items: flex-start;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-summary-header"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-summary-title"] {
|
||||||
|
font-size: 12px; /* text-12-medium */
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-weak);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-markdown"] {
|
||||||
|
&[data-diffs="true"] {
|
||||||
|
font-size: 14px; /* text-14-regular */
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-fade="true"] > * {
|
||||||
|
animation: fade-up-text 0.3s ease-out forwards;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-accordion"] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="sticky-accordion-header"] {
|
||||||
|
top: 40px;
|
||||||
|
|
||||||
|
&[data-expanded]::before {
|
||||||
|
top: -40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-accordion-trigger-content"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-file-info"] {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-file-icon"] {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-file-path"] {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-directory"] {
|
||||||
|
color: var(--text-base);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
direction: rtl;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-filename"] {
|
||||||
|
color: var(--text-strong);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-accordion-actions"] {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-accordion-content"] {
|
||||||
|
max-height: 240px; /* max-h-60 */
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-accordion-content"]::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-response-section"] {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-collapsible-trigger-content"] {
|
||||||
|
color: var(--text-weak);
|
||||||
|
cursor: pointer;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--text-strong);
|
||||||
|
}
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-details-text"] {
|
||||||
|
font-size: 12px; /* text-12-medium */
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card {
|
||||||
|
color: var(--text-on-critical-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-collapsible-content-inner"] {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-self: stretch;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
import { AssistantMessage } from "@opencode-ai/sdk"
|
||||||
|
import { useData } from "../context"
|
||||||
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
|
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||||
|
import { createEffect, createMemo, createSignal, For, Match, ParentProps, Show, Switch } from "solid-js"
|
||||||
|
import { DiffChanges } from "./diff-changes"
|
||||||
|
import { Typewriter } from "./typewriter"
|
||||||
|
import { Message } from "./message-part"
|
||||||
|
import { Markdown } from "./markdown"
|
||||||
|
import { Accordion } from "./accordion"
|
||||||
|
import { StickyAccordionHeader } from "./sticky-accordion-header"
|
||||||
|
import { FileIcon } from "./file-icon"
|
||||||
|
import { Icon } from "./icon"
|
||||||
|
import { Diff } from "./diff"
|
||||||
|
import { Card } from "./card"
|
||||||
|
import { MessageProgress } from "./message-progress"
|
||||||
|
import { Collapsible } from "./collapsible"
|
||||||
|
|
||||||
|
export function SessionTurn(
|
||||||
|
props: ParentProps<{
|
||||||
|
sessionID: string
|
||||||
|
messageID: string
|
||||||
|
classes?: {
|
||||||
|
root?: string
|
||||||
|
content?: string
|
||||||
|
container?: string
|
||||||
|
}
|
||||||
|
}>,
|
||||||
|
) {
|
||||||
|
const data = useData()
|
||||||
|
const match = Binary.search(data.session, props.sessionID, (s) => s.id)
|
||||||
|
if (!match.found) throw new Error(`Session ${props.sessionID} not found`)
|
||||||
|
|
||||||
|
const messages = createMemo(() => (props.sessionID ? (data.message[props.sessionID] ?? []) : []))
|
||||||
|
const userMessages = createMemo(() =>
|
||||||
|
messages()
|
||||||
|
.filter((m) => m.role === "user")
|
||||||
|
.sort((a, b) => b.id.localeCompare(a.id)),
|
||||||
|
)
|
||||||
|
const lastUserMessage = createMemo(() => {
|
||||||
|
return userMessages()?.at(0)
|
||||||
|
})
|
||||||
|
const message = createMemo(() => userMessages()?.find((m) => m.id === props.messageID))
|
||||||
|
|
||||||
|
const status = createMemo(
|
||||||
|
() =>
|
||||||
|
data.session_status[props.sessionID] ?? {
|
||||||
|
type: "idle",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const working = createMemo(() => status()?.type !== "idle")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-component="session-turn" class={props.classes?.root}>
|
||||||
|
<div data-slot="session-turn-content" class={props.classes?.content}>
|
||||||
|
<Show when={message()}>
|
||||||
|
{(msg) => {
|
||||||
|
const titleSeen = createMemo(() => true)
|
||||||
|
const contentSeen = createMemo(() => true)
|
||||||
|
|
||||||
|
const [titled, setTitled] = createSignal(titleSeen())
|
||||||
|
const assistantMessages = createMemo(() => {
|
||||||
|
return messages()?.filter((m) => m.role === "assistant" && m.parentID == msg().id) as AssistantMessage[]
|
||||||
|
})
|
||||||
|
const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
|
||||||
|
const [detailsExpanded, setDetailsExpanded] = createSignal(false)
|
||||||
|
const parts = createMemo(() => data.part[msg().id])
|
||||||
|
const hasToolPart = createMemo(() =>
|
||||||
|
assistantMessages()
|
||||||
|
?.flatMap((m) => data.part[m.id])
|
||||||
|
.some((p) => p?.type === "tool"),
|
||||||
|
)
|
||||||
|
const messageWorking = createMemo(() => msg().id === lastUserMessage()?.id && working())
|
||||||
|
const initialCompleted = !(msg().id === lastUserMessage()?.id && working())
|
||||||
|
const [completed, setCompleted] = createSignal(initialCompleted)
|
||||||
|
|
||||||
|
// allowing time for the animations to finish
|
||||||
|
createEffect(() => {
|
||||||
|
if (titleSeen()) return
|
||||||
|
const title = msg().summary?.title
|
||||||
|
if (title) setTimeout(() => setTitled(true), 10_000)
|
||||||
|
})
|
||||||
|
createEffect(() => {
|
||||||
|
const completed = !messageWorking()
|
||||||
|
setTimeout(() => setCompleted(completed), 1200)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-message={msg().id} data-slot="session-turn-message-container" class={props.classes?.container}>
|
||||||
|
{/* Title */}
|
||||||
|
<div data-slot="session-turn-message-header">
|
||||||
|
<div data-slot="session-turn-message-title">
|
||||||
|
<Show
|
||||||
|
when={titled()}
|
||||||
|
fallback={<Typewriter as="h1" text={msg().summary?.title} data-slot="session-turn-typewriter" />}
|
||||||
|
>
|
||||||
|
<h1>{msg().summary?.title}</h1>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div data-slot="session-turn-message-content">
|
||||||
|
<Message message={msg()} parts={parts()} />
|
||||||
|
</div>
|
||||||
|
{/* Summary */}
|
||||||
|
<Show when={completed()}>
|
||||||
|
<div data-slot="session-turn-summary-section">
|
||||||
|
<div data-slot="session-turn-summary-header">
|
||||||
|
<h2 data-slot="session-turn-summary-title">
|
||||||
|
<Switch>
|
||||||
|
<Match when={msg().summary?.diffs?.length}>Summary</Match>
|
||||||
|
<Match when={true}>Response</Match>
|
||||||
|
</Switch>
|
||||||
|
</h2>
|
||||||
|
<Show when={msg().summary?.body}>
|
||||||
|
{(summary) => (
|
||||||
|
<Markdown
|
||||||
|
data-slot="session-turn-markdown"
|
||||||
|
data-diffs={!!msg().summary?.diffs?.length}
|
||||||
|
data-fade={!msg().summary?.diffs?.length && !contentSeen()}
|
||||||
|
text={summary()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<Accordion data-slot="session-turn-accordion" multiple>
|
||||||
|
<For each={msg().summary?.diffs ?? []}>
|
||||||
|
{(diff) => (
|
||||||
|
<Accordion.Item value={diff.file}>
|
||||||
|
<StickyAccordionHeader>
|
||||||
|
<Accordion.Trigger>
|
||||||
|
<div data-slot="session-turn-accordion-trigger-content">
|
||||||
|
<div data-slot="session-turn-file-info">
|
||||||
|
<FileIcon
|
||||||
|
node={{ path: diff.file, type: "file" }}
|
||||||
|
data-slot="session-turn-file-icon"
|
||||||
|
/>
|
||||||
|
<div data-slot="session-turn-file-path">
|
||||||
|
<Show when={diff.file.includes("/")}>
|
||||||
|
<span data-slot="session-turn-directory">{getDirectory(diff.file)}‎</span>
|
||||||
|
</Show>
|
||||||
|
<span data-slot="session-turn-filename">{getFilename(diff.file)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div data-slot="session-turn-accordion-actions">
|
||||||
|
<DiffChanges changes={diff} />
|
||||||
|
<Icon name="chevron-grabber-vertical" size="small" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Accordion.Trigger>
|
||||||
|
</StickyAccordionHeader>
|
||||||
|
<Accordion.Content data-slot="session-turn-accordion-content">
|
||||||
|
<Diff
|
||||||
|
before={{
|
||||||
|
name: diff.file!,
|
||||||
|
contents: diff.before!,
|
||||||
|
}}
|
||||||
|
after={{
|
||||||
|
name: diff.file!,
|
||||||
|
contents: diff.after!,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Accordion.Content>
|
||||||
|
</Accordion.Item>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={error() && !detailsExpanded()}>
|
||||||
|
<Card variant="error" class="error-card">
|
||||||
|
{error()?.data?.message as string}
|
||||||
|
</Card>
|
||||||
|
</Show>
|
||||||
|
{/* Response */}
|
||||||
|
<div data-slot="session-turn-response-section">
|
||||||
|
<Switch>
|
||||||
|
<Match when={!completed()}>
|
||||||
|
<MessageProgress assistantMessages={assistantMessages} done={!messageWorking()} />
|
||||||
|
</Match>
|
||||||
|
<Match when={completed() && hasToolPart()}>
|
||||||
|
<Collapsible variant="ghost" open={detailsExpanded()} onOpenChange={setDetailsExpanded}>
|
||||||
|
<Collapsible.Trigger>
|
||||||
|
<div data-slot="session-turn-collapsible-trigger-content">
|
||||||
|
<div data-slot="session-turn-details-text">
|
||||||
|
<Switch>
|
||||||
|
<Match when={detailsExpanded()}>Hide details</Match>
|
||||||
|
<Match when={!detailsExpanded()}>Show details</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
<Collapsible.Arrow />
|
||||||
|
</div>
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<Collapsible.Content>
|
||||||
|
<div data-slot="session-turn-collapsible-content-inner">
|
||||||
|
<For each={assistantMessages()}>
|
||||||
|
{(assistantMessage) => {
|
||||||
|
const parts = createMemo(() => data.part[assistantMessage.id])
|
||||||
|
return <Message message={assistantMessage} parts={parts()} />
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
<Show when={error()}>
|
||||||
|
<Card variant="error" class="error-card">
|
||||||
|
{error()?.data?.message as string}
|
||||||
|
</Card>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Show>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -22,12 +22,14 @@
|
|||||||
@import "../components/logo.css" layer(components);
|
@import "../components/logo.css" layer(components);
|
||||||
@import "../components/markdown.css" layer(components);
|
@import "../components/markdown.css" layer(components);
|
||||||
@import "../components/message-part.css" layer(components);
|
@import "../components/message-part.css" layer(components);
|
||||||
|
@import "../components/message-progress.css" layer(components);
|
||||||
|
@import "../components/message-nav.css" layer(components);
|
||||||
@import "../components/progress-circle.css" layer(components);
|
@import "../components/progress-circle.css" layer(components);
|
||||||
@import "../components/select.css" layer(components);
|
@import "../components/select.css" layer(components);
|
||||||
@import "../components/select-dialog.css" layer(components);
|
@import "../components/select-dialog.css" layer(components);
|
||||||
@import "../components/spinner.css" layer(components);
|
@import "../components/spinner.css" layer(components);
|
||||||
@import "../components/session-review.css" layer(components);
|
@import "../components/session-review.css" layer(components);
|
||||||
@import "../components/session-timeline.css" layer(components);
|
@import "../components/session-turn.css" layer(components);
|
||||||
@import "../components/sticky-accordion-header.css" layer(components);
|
@import "../components/sticky-accordion-header.css" layer(components);
|
||||||
@import "../components/tabs.css" layer(components);
|
@import "../components/tabs.css" layer(components);
|
||||||
@import "../components/tooltip.css" layer(components);
|
@import "../components/tooltip.css" layer(components);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/util",
|
"name": "@opencode-ai/util",
|
||||||
"version": "1.0.102",
|
"version": "1.0.107",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.0.102",
|
"version": "1.0.107",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
"info": { "$ref": "#/definitions/colorValue" },
|
"info": { "$ref": "#/definitions/colorValue" },
|
||||||
"text": { "$ref": "#/definitions/colorValue" },
|
"text": { "$ref": "#/definitions/colorValue" },
|
||||||
"textMuted": { "$ref": "#/definitions/colorValue" },
|
"textMuted": { "$ref": "#/definitions/colorValue" },
|
||||||
|
"selectedListItemText": { "$ref": "#/definitions/colorValue" },
|
||||||
"background": { "$ref": "#/definitions/colorValue" },
|
"background": { "$ref": "#/definitions/colorValue" },
|
||||||
"backgroundPanel": { "$ref": "#/definitions/colorValue" },
|
"backgroundPanel": { "$ref": "#/definitions/colorValue" },
|
||||||
"backgroundElement": { "$ref": "#/definitions/colorValue" },
|
"backgroundElement": { "$ref": "#/definitions/colorValue" },
|
||||||
|
|||||||
@@ -112,6 +112,12 @@ You can also install it with the following commands:
|
|||||||
mise use --pin -g ubi:sst/opencode
|
mise use --pin -g ubi:sst/opencode
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- **Using Docker**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -it --rm ghcr.io/sst/opencode
|
||||||
|
```
|
||||||
|
|
||||||
Support for installing OpenCode on Windows using Bun is currently in progress.
|
Support for installing OpenCode on Windows using Bun is currently in progress.
|
||||||
|
|
||||||
You can also grab the binary from the [Releases](https://github.com/sst/opencode/releases).
|
You can also grab the binary from the [Releases](https://github.com/sst/opencode/releases).
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
diff --git a/dist/server/StartServer.jsx b/dist/server/StartServer.jsx
|
|
||||||
index 0c6a9fe3121a88520db252570a8b5997a36e0452..b3479cdb78c70d7a655d302d8e6ab536b4ff07a3 100644
|
|
||||||
--- a/dist/server/StartServer.jsx
|
|
||||||
+++ b/dist/server/StartServer.jsx
|
|
||||||
@@ -52,11 +52,11 @@ export function StartServer(props) {
|
|
||||||
else if (import.meta.env.DEV)
|
|
||||||
console.warn("No route matched for preloading js assets");
|
|
||||||
}
|
|
||||||
- assets = await Promise.all(assetPromises).then(a =>
|
|
||||||
+ assets = await Promise.all(assetPromises).then(a =>
|
|
||||||
// dedupe assets
|
|
||||||
[...new Map(a.flat().map(item => [item.attrs.key, item])).values()].filter(asset => import.meta.env.START_ISLANDS
|
|
||||||
? false
|
|
||||||
- : asset.attrs.rel === "modulepreload" &&
|
|
||||||
+ : (asset.attrs.rel === "modulepreload" || asset.attrs.rel === "stylesheet") &&
|
|
||||||
!context.assets.find((a) => a.attrs.key === asset.attrs.key)));
|
|
||||||
});
|
|
||||||
useAssets(() => (assets.length ? assets.map(m => renderAsset(m)) : undefined));
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"displayName": "opencode",
|
"displayName": "opencode",
|
||||||
"description": "opencode for VS Code",
|
"description": "opencode for VS Code",
|
||||||
"version": "1.0.102",
|
"version": "1.0.107",
|
||||||
"publisher": "sst-dev",
|
"publisher": "sst-dev",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
Vendored
+16
@@ -49,6 +49,10 @@ declare module "sst" {
|
|||||||
type: "sst.cloudflare.SolidStart"
|
type: "sst.cloudflare.SolidStart"
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
ConsoleData: {
|
||||||
|
name: string
|
||||||
|
type: "sst.cloudflare.Bucket"
|
||||||
|
}
|
||||||
Database: {
|
Database: {
|
||||||
database: string
|
database: string
|
||||||
host: string
|
host: string
|
||||||
@@ -65,6 +69,10 @@ declare module "sst" {
|
|||||||
type: "sst.sst.Secret"
|
type: "sst.sst.Secret"
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
EnterpriseStorage: {
|
||||||
|
name: string
|
||||||
|
type: "sst.cloudflare.Bucket"
|
||||||
|
}
|
||||||
GITHUB_APP_ID: {
|
GITHUB_APP_ID: {
|
||||||
type: "sst.sst.Secret"
|
type: "sst.sst.Secret"
|
||||||
value: string
|
value: string
|
||||||
@@ -96,6 +104,14 @@ declare module "sst" {
|
|||||||
LogProcessor: {
|
LogProcessor: {
|
||||||
type: "sst.cloudflare.Worker"
|
type: "sst.cloudflare.Worker"
|
||||||
}
|
}
|
||||||
|
R2AccessKey: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
R2SecretKey: {
|
||||||
|
type: "sst.sst.Secret"
|
||||||
|
value: string
|
||||||
|
}
|
||||||
STRIPE_SECRET_KEY: {
|
STRIPE_SECRET_KEY: {
|
||||||
type: "sst.sst.Secret"
|
type: "sst.sst.Secret"
|
||||||
value: string
|
value: string
|
||||||
|
|||||||
+2
-1
@@ -9,7 +9,7 @@ export default $config({
|
|||||||
home: "cloudflare",
|
home: "cloudflare",
|
||||||
providers: {
|
providers: {
|
||||||
stripe: {
|
stripe: {
|
||||||
apiKey: process.env.STRIPE_SECRET_KEY,
|
apiKey: process.env.STRIPE_SECRET_KEY!,
|
||||||
},
|
},
|
||||||
planetscale: "0.4.1",
|
planetscale: "0.4.1",
|
||||||
},
|
},
|
||||||
@@ -19,5 +19,6 @@ export default $config({
|
|||||||
await import("./infra/app.js")
|
await import("./infra/app.js")
|
||||||
await import("./infra/console.js")
|
await import("./infra/console.js")
|
||||||
await import("./infra/desktop.js")
|
await import("./infra/desktop.js")
|
||||||
|
await import("./infra/enterprise.js")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user