Merge branch 'dev' into llm-centralization
commit
106a6bb08c
1
STATS.md
1
STATS.md
|
|
@ -167,3 +167,4 @@
|
|||
| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) |
|
||||
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
|
||||
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
|
||||
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
|
||||
|
|
|
|||
12
bun.lock
12
bun.lock
|
|
@ -131,6 +131,7 @@
|
|||
"@opencode-ai/util": "workspace:*",
|
||||
"@shikijs/transformers": "3.9.2",
|
||||
"@solid-primitives/active-element": "2.1.3",
|
||||
"@solid-primitives/audio": "1.4.2",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
"@solid-primitives/resize-observer": "2.1.3",
|
||||
"@solid-primitives/scroll": "2.1.3",
|
||||
|
|
@ -355,9 +356,12 @@
|
|||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-dialog": "~2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"@tauri-apps/plugin-os": "~2",
|
||||
"@tauri-apps/plugin-process": "~2",
|
||||
"@tauri-apps/plugin-shell": "~2",
|
||||
"@tauri-apps/plugin-store": "~2",
|
||||
"@tauri-apps/plugin-updater": "~2",
|
||||
"@tauri-apps/plugin-window-state": "~2",
|
||||
"solid-js": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -1546,6 +1550,8 @@
|
|||
|
||||
"@solid-primitives/active-element": ["@solid-primitives/active-element@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9t5K4aR2naVDj950XU8OjnLgOg94a8k5wr6JNOPK+N5ESLsJDq42c1ZP8UKpewi1R+wplMMxiM6OPKRzbxJY7A=="],
|
||||
|
||||
"@solid-primitives/audio": ["@solid-primitives/audio@1.4.2", "", { "dependencies": { "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-UMD3ORQfI5Ky8yuKPxidDiEazsjv/dsoiKK5yZxLnsgaeNR1Aym3/77h/qT1jBYeXUgj4DX6t7NMpFUSVr14OQ=="],
|
||||
|
||||
"@solid-primitives/event-bus": ["@solid-primitives/event-bus@1.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-l+n10/51neGcMaP3ypYt21bXfoeWh8IaC8k7fYuY3ww2a8S1Zv2N2a7FF5Qn+waTu86l0V8/nRHjkyqVIZBYwA=="],
|
||||
|
||||
"@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="],
|
||||
|
|
@ -1658,12 +1664,18 @@
|
|||
|
||||
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew=="],
|
||||
|
||||
"@tauri-apps/plugin-os": ["@tauri-apps/plugin-os@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A=="],
|
||||
|
||||
"@tauri-apps/plugin-process": ["@tauri-apps/plugin-process@2.3.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA=="],
|
||||
|
||||
"@tauri-apps/plugin-shell": ["@tauri-apps/plugin-shell@2.3.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Xod+pRcFxmOWFWEnqH5yZcA7qwAMuaaDkMR1Sply+F8VfBj++CGnj2xf5UoialmjZ2Cvd8qrvSCbU+7GgNVsKQ=="],
|
||||
|
||||
"@tauri-apps/plugin-store": ["@tauri-apps/plugin-store@2.4.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ckGSEzZ5Ii4Hf2D5x25Oqnm2Zf9MfDWAzR+volY0z/OOBz6aucPKEY0F649JvQ0Vupku6UJo7ugpGRDOFOunkA=="],
|
||||
|
||||
"@tauri-apps/plugin-updater": ["@tauri-apps/plugin-updater@2.9.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg=="],
|
||||
|
||||
"@tauri-apps/plugin-window-state": ["@tauri-apps/plugin-window-state@2.4.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-OuvdrzyY8Q5Dbzpj+GcrnV1iCeoZbcFdzMjanZMMcAEUNy/6PH5pxZPXpaZLOR7whlzXiuzx0L9EKZbH7zpdRw=="],
|
||||
|
||||
"@thisbeyond/solid-dnd": ["@thisbeyond/solid-dnd@0.7.5", "", { "peerDependencies": { "solid-js": "^1.5" } }, "sha512-DfI5ff+yYGpK9M21LhYwIPlbP2msKxN2ARwuu6GF8tT1GgNVDTI8VCQvH4TJFoVApP9d44izmAcTh/iTCH2UUw=="],
|
||||
|
||||
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1765270179,
|
||||
"narHash": "sha256-g2a4MhRKu4ymR4xwo+I+auTknXt/+j37Lnf0Mvfl1rE=",
|
||||
"lastModified": 1765425892,
|
||||
"narHash": "sha256-jlQpSkg2sK6IJVzTQBDyRxQZgKADC2HKMRfGCSgNMHo=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "677fbe97984e7af3175b6c121f3c39ee5c8d62c9",
|
||||
"rev": "5d6bdbddb4695a62f0d00a3620b37a15275a5093",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ const ZEN_MODELS = [
|
|||
new sst.Secret("ZEN_MODELS2"),
|
||||
new sst.Secret("ZEN_MODELS3"),
|
||||
new sst.Secret("ZEN_MODELS4"),
|
||||
new sst.Secret("ZEN_MODELS5"),
|
||||
]
|
||||
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
||||
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"nodeModules": "sha256-3GaqUwomnIUW8MqUi1jDVPHQ/C5Z+D9wMR//tAGxvSQ="
|
||||
"nodeModules": "sha256-3CG0wAMQp2E6ghPUXbYaYifJorp9b1WvCtHD+o8Nhck="
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export const github = query(async () => {
|
|||
release: {
|
||||
name: release.name,
|
||||
url: release.html_url,
|
||||
tag_name: release.tag_name,
|
||||
},
|
||||
contributors: contributorCount,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,15 +8,7 @@ import { Faq } from "~/component/faq"
|
|||
import desktopAppIcon from "../../asset/lander/opencode-desktop-icon.png"
|
||||
import { Legal } from "~/component/legal"
|
||||
import { config } from "~/config"
|
||||
import { createMemo } from "solid-js"
|
||||
|
||||
const getLatestRelease = query(async () => {
|
||||
"use server"
|
||||
const response = await fetch("https://api.github.com/repos/sst/opencode/releases/latest")
|
||||
if (!response.ok) return null
|
||||
const data = await response.json()
|
||||
return data.tag_name as string
|
||||
}, "latest-release")
|
||||
import { github } from "~/lib/github"
|
||||
|
||||
function CopyStatus() {
|
||||
return (
|
||||
|
|
@ -28,14 +20,14 @@ function CopyStatus() {
|
|||
}
|
||||
|
||||
export default function Download() {
|
||||
const release = createAsync(() => getLatestRelease(), {
|
||||
const githubData = createAsync(() => github(), {
|
||||
deferStream: true,
|
||||
})
|
||||
const download = createMemo(() => {
|
||||
const version = release() ?? "v1.0.150"
|
||||
const download = () => {
|
||||
const version = githubData()?.release.tag_name
|
||||
if (!version) return null
|
||||
return `https://github.com/sst/opencode/releases/download/${version}`
|
||||
})
|
||||
}
|
||||
const handleCopyClick = (command: string) => (event: Event) => {
|
||||
const button = event.currentTarget as HTMLButtonElement
|
||||
navigator.clipboard.writeText(command)
|
||||
|
|
|
|||
|
|
@ -43,9 +43,12 @@ const getModelsInfo = query(async (workspaceID: string) => {
|
|||
const pA = getPriority(idA)
|
||||
const pB = getPriority(idB)
|
||||
if (pA !== pB) return pA - pB
|
||||
return modelA.name.localeCompare(modelB.name)
|
||||
|
||||
const modelAName = Array.isArray(modelA) ? modelA[0].name : modelA.name
|
||||
const modelBName = Array.isArray(modelB) ? modelB[0].name : modelB.name
|
||||
return modelAName.localeCompare(modelBName)
|
||||
})
|
||||
.map(([id, model]) => ({ id, name: model.name })),
|
||||
.map(([id, model]) => ({ id, name: Array.isArray(model) ? model[0].name : model.name })),
|
||||
disabled: await Model.listDisabled(),
|
||||
}
|
||||
}, workspaceID)
|
||||
|
|
|
|||
|
|
@ -57,15 +57,17 @@ export async function handler(
|
|||
const sessionId = input.request.headers.get("x-opencode-session") ?? ""
|
||||
const requestId = input.request.headers.get("x-opencode-request") ?? ""
|
||||
const projectId = input.request.headers.get("x-opencode-project") ?? ""
|
||||
const ocClient = input.request.headers.get("x-opencode-client") ?? ""
|
||||
logger.metric({
|
||||
is_tream: isStream,
|
||||
session: sessionId,
|
||||
request: requestId,
|
||||
client: ocClient,
|
||||
})
|
||||
const zenData = ZenData.list()
|
||||
const modelInfo = validateModel(zenData, model)
|
||||
const dataDumper = createDataDumper(sessionId, requestId, projectId)
|
||||
const trialLimiter = createTrialLimiter(modelInfo.trial?.limit, ip)
|
||||
const trialLimiter = createTrialLimiter(modelInfo.trial, ip, ocClient)
|
||||
const isTrial = await trialLimiter?.isTrial()
|
||||
const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
|
||||
await rateLimiter?.check()
|
||||
|
|
@ -286,11 +288,14 @@ export async function handler(
|
|||
}
|
||||
|
||||
function validateModel(zenData: ZenData, reqModel: string) {
|
||||
if (!(reqModel in zenData.models)) {
|
||||
throw new ModelError(`Model ${reqModel} not supported`)
|
||||
}
|
||||
if (!(reqModel in zenData.models)) throw new ModelError(`Model ${reqModel} not supported`)
|
||||
|
||||
const modelId = reqModel as keyof typeof zenData.models
|
||||
const modelData = zenData.models[modelId]
|
||||
const modelData = Array.isArray(zenData.models[modelId])
|
||||
? zenData.models[modelId].find((model) => opts.format === model.formatFilter)
|
||||
: zenData.models[modelId]
|
||||
|
||||
if (!modelData) throw new ModelError(`Model ${reqModel} not supported for format ${opts.format}`)
|
||||
|
||||
logger.metric({ model: modelId })
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
import { Database, eq, sql } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { IpTable } from "@opencode-ai/console-core/schema/ip.sql.js"
|
||||
import { UsageInfo } from "./provider/provider"
|
||||
import { ZenData } from "@opencode-ai/console-core/model.js"
|
||||
|
||||
export function createTrialLimiter(limit: number | undefined, ip: string) {
|
||||
if (!limit) return
|
||||
export function createTrialLimiter(trial: ZenData.Trial | undefined, ip: string, client: string) {
|
||||
if (!trial) return
|
||||
if (!ip) return
|
||||
|
||||
let trial: boolean
|
||||
const limit =
|
||||
trial.limits.find((limit) => limit.client === client)?.limit ??
|
||||
trial.limits.find((limit) => limit.client === undefined)?.limit
|
||||
if (!limit) return
|
||||
|
||||
let _isTrial: boolean
|
||||
|
||||
return {
|
||||
isTrial: async () => {
|
||||
|
|
@ -20,11 +26,11 @@ export function createTrialLimiter(limit: number | undefined, ip: string) {
|
|||
.then((rows) => rows[0]),
|
||||
)
|
||||
|
||||
trial = (data?.usage ?? 0) < limit
|
||||
return trial
|
||||
_isTrial = (data?.usage ?? 0) < limit
|
||||
return _isTrial
|
||||
},
|
||||
track: async (usageInfo: UsageInfo) => {
|
||||
if (!trial) return
|
||||
if (!_isTrial) return
|
||||
const usage =
|
||||
usageInfo.inputTokens +
|
||||
usageInfo.outputTokens +
|
||||
|
|
|
|||
|
|
@ -16,16 +16,19 @@ const value1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[
|
|||
const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
|
||||
const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
|
||||
const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
||||
const value5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
|
||||
if (!value1) throw new Error("ZEN_MODELS1 not found")
|
||||
if (!value2) throw new Error("ZEN_MODELS2 not found")
|
||||
if (!value3) throw new Error("ZEN_MODELS3 not found")
|
||||
if (!value4) throw new Error("ZEN_MODELS4 not found")
|
||||
if (!value5) throw new Error("ZEN_MODELS5 not found")
|
||||
|
||||
// validate value
|
||||
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4))
|
||||
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5))
|
||||
|
||||
// update the secret
|
||||
await $`bun sst secret set ZEN_MODELS1 ${value1} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS2 ${value2} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS3 ${value3} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS4 ${value4} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS5 ${value5} --stage ${stage}`
|
||||
|
|
|
|||
|
|
@ -16,16 +16,19 @@ const value1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[
|
|||
const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
|
||||
const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
|
||||
const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
||||
const value5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
|
||||
if (!value1) throw new Error("ZEN_MODELS1 not found")
|
||||
if (!value2) throw new Error("ZEN_MODELS2 not found")
|
||||
if (!value3) throw new Error("ZEN_MODELS3 not found")
|
||||
if (!value4) throw new Error("ZEN_MODELS4 not found")
|
||||
if (!value5) throw new Error("ZEN_MODELS5 not found")
|
||||
|
||||
// validate value
|
||||
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4))
|
||||
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5))
|
||||
|
||||
// update the secret
|
||||
await $`bun sst secret set ZEN_MODELS1 ${value1}`
|
||||
await $`bun sst secret set ZEN_MODELS2 ${value2}`
|
||||
await $`bun sst secret set ZEN_MODELS3 ${value3}`
|
||||
await $`bun sst secret set ZEN_MODELS4 ${value4}`
|
||||
await $`bun sst secret set ZEN_MODELS5 ${value5}`
|
||||
|
|
|
|||
|
|
@ -14,15 +14,17 @@ const oldValue1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=
|
|||
const oldValue2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
|
||||
const oldValue3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
|
||||
const oldValue4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
||||
const oldValue5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
|
||||
if (!oldValue1) throw new Error("ZEN_MODELS1 not found")
|
||||
if (!oldValue2) throw new Error("ZEN_MODELS2 not found")
|
||||
if (!oldValue3) throw new Error("ZEN_MODELS3 not found")
|
||||
if (!oldValue4) throw new Error("ZEN_MODELS4 not found")
|
||||
if (!oldValue5) throw new Error("ZEN_MODELS5 not found")
|
||||
|
||||
// store the prettified json to a temp file
|
||||
const filename = `models-${Date.now()}.json`
|
||||
const tempFile = Bun.file(path.join(os.tmpdir(), filename))
|
||||
await tempFile.write(JSON.stringify(JSON.parse(oldValue1 + oldValue2 + oldValue3 + oldValue4), null, 2))
|
||||
await tempFile.write(JSON.stringify(JSON.parse(oldValue1 + oldValue2 + oldValue3 + oldValue4 + oldValue5), null, 2))
|
||||
console.log("tempFile", tempFile.name)
|
||||
|
||||
// open temp file in vim and read the file on close
|
||||
|
|
@ -31,12 +33,15 @@ const newValue = JSON.stringify(JSON.parse(await tempFile.text()))
|
|||
ZenData.validate(JSON.parse(newValue))
|
||||
|
||||
// update the secret
|
||||
const chunk = Math.ceil(newValue.length / 4)
|
||||
const chunk = Math.ceil(newValue.length / 5)
|
||||
const newValue1 = newValue.slice(0, chunk)
|
||||
const newValue2 = newValue.slice(chunk, chunk * 2)
|
||||
const newValue3 = newValue.slice(chunk * 2, chunk * 3)
|
||||
const newValue4 = newValue.slice(chunk * 3)
|
||||
const newValue4 = newValue.slice(chunk * 3, chunk * 4)
|
||||
const newValue5 = newValue.slice(chunk * 4)
|
||||
|
||||
await $`bun sst secret set ZEN_MODELS1 ${newValue1}`
|
||||
await $`bun sst secret set ZEN_MODELS2 ${newValue2}`
|
||||
await $`bun sst secret set ZEN_MODELS3 ${newValue3}`
|
||||
await $`bun sst secret set ZEN_MODELS4 ${newValue4}`
|
||||
await $`bun sst secret set ZEN_MODELS5 ${newValue5}`
|
||||
|
|
|
|||
|
|
@ -9,7 +9,17 @@ import { Resource } from "@opencode-ai/console-resource"
|
|||
|
||||
export namespace ZenData {
|
||||
const FormatSchema = z.enum(["anthropic", "google", "openai", "oa-compat"])
|
||||
const TrialSchema = z.object({
|
||||
provider: z.string(),
|
||||
limits: z.array(
|
||||
z.object({
|
||||
limit: z.number(),
|
||||
client: z.enum(["cli", "desktop"]).optional(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
export type Format = z.infer<typeof FormatSchema>
|
||||
export type Trial = z.infer<typeof TrialSchema>
|
||||
|
||||
const ModelCostSchema = z.object({
|
||||
input: z.number(),
|
||||
|
|
@ -26,12 +36,7 @@ export namespace ZenData {
|
|||
allowAnonymous: z.boolean().optional(),
|
||||
byokProvider: z.enum(["openai", "anthropic", "google"]).optional(),
|
||||
stickyProvider: z.boolean().optional(),
|
||||
trial: z
|
||||
.object({
|
||||
limit: z.number(),
|
||||
provider: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
trial: TrialSchema.optional(),
|
||||
rateLimit: z.number().optional(),
|
||||
fallbackProvider: z.string().optional(),
|
||||
providers: z.array(
|
||||
|
|
@ -53,7 +58,7 @@ export namespace ZenData {
|
|||
})
|
||||
|
||||
const ModelsSchema = z.object({
|
||||
models: z.record(z.string(), ModelSchema),
|
||||
models: z.record(z.string(), z.union([ModelSchema, z.array(ModelSchema.extend({ formatFilter: FormatSchema }))])),
|
||||
providers: z.record(z.string(), ProviderSchema),
|
||||
})
|
||||
|
||||
|
|
@ -63,7 +68,11 @@ export namespace ZenData {
|
|||
|
||||
export const list = fn(z.void(), () => {
|
||||
const json = JSON.parse(
|
||||
Resource.ZEN_MODELS1.value + Resource.ZEN_MODELS2.value + Resource.ZEN_MODELS3.value + Resource.ZEN_MODELS4.value,
|
||||
Resource.ZEN_MODELS1.value +
|
||||
Resource.ZEN_MODELS2.value +
|
||||
Resource.ZEN_MODELS3.value +
|
||||
Resource.ZEN_MODELS4.value +
|
||||
Resource.ZEN_MODELS5.value,
|
||||
)
|
||||
return ModelsSchema.parse(json)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -50,10 +50,6 @@ declare module "sst" {
|
|||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"Enterprise": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
"GITHUB_APP_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
|
@ -94,6 +90,10 @@ declare module "sst" {
|
|||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
"Teams": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
"Web": {
|
||||
"type": "sst.cloudflare.Astro"
|
||||
"url": string
|
||||
|
|
@ -114,6 +114,10 @@ declare module "sst" {
|
|||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS5": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
|
|
|
|||
|
|
@ -50,10 +50,6 @@ declare module "sst" {
|
|||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"Enterprise": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
"GITHUB_APP_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
|
@ -94,6 +90,10 @@ declare module "sst" {
|
|||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
"Teams": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
"Web": {
|
||||
"type": "sst.cloudflare.Astro"
|
||||
"url": string
|
||||
|
|
@ -114,6 +114,10 @@ declare module "sst" {
|
|||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS5": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
|
|
|
|||
|
|
@ -50,10 +50,6 @@ declare module "sst" {
|
|||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"Enterprise": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
"GITHUB_APP_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
|
@ -94,6 +90,10 @@ declare module "sst" {
|
|||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
"Teams": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
"Web": {
|
||||
"type": "sst.cloudflare.Astro"
|
||||
"url": string
|
||||
|
|
@ -114,6 +114,10 @@ declare module "sst" {
|
|||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS5": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
<meta property="og:image" content="/social-share.png" />
|
||||
<meta property="twitter:image" content="/social-share.png" />
|
||||
</head>
|
||||
<body class="antialiased overscroll-none select-none text-12-regular">
|
||||
<body class="antialiased overscroll-none select-none text-12-regular overflow-hidden">
|
||||
<script>
|
||||
;(function () {
|
||||
const savedTheme = localStorage.getItem("theme") || "oc-1"
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
})()
|
||||
</script>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<div id="root" class="flex flex-col h-screen"></div>
|
||||
<script src="/src/entry.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
"@opencode-ai/util": "workspace:*",
|
||||
"@shikijs/transformers": "3.9.2",
|
||||
"@solid-primitives/active-element": "2.1.3",
|
||||
"@solid-primitives/audio": "1.4.2",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
"@solid-primitives/resize-observer": "2.1.3",
|
||||
"@solid-primitives/scroll": "2.1.3",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { LayoutProvider } from "./context/layout"
|
|||
import { GlobalSDKProvider } from "./context/global-sdk"
|
||||
import { SessionProvider } from "./context/session"
|
||||
import { Show } from "solid-js"
|
||||
import { NotificationProvider } from "./context/notification"
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
|
@ -37,25 +38,27 @@ export function App() {
|
|||
<GlobalSDKProvider url={url}>
|
||||
<GlobalSyncProvider>
|
||||
<LayoutProvider>
|
||||
<MetaProvider>
|
||||
<Font />
|
||||
<Router root={Layout}>
|
||||
<Route path="/" component={Home} />
|
||||
<Route path="/:dir" component={DirectoryLayout}>
|
||||
<Route path="/" component={() => <Navigate href="session" />} />
|
||||
<Route
|
||||
path="/session/:id?"
|
||||
component={(p) => (
|
||||
<Show when={p.params.id || true} keyed>
|
||||
<SessionProvider>
|
||||
<Session />
|
||||
</SessionProvider>
|
||||
</Show>
|
||||
)}
|
||||
/>
|
||||
</Route>
|
||||
</Router>
|
||||
</MetaProvider>
|
||||
<NotificationProvider>
|
||||
<MetaProvider>
|
||||
<Font />
|
||||
<Router root={Layout}>
|
||||
<Route path="/" component={Home} />
|
||||
<Route path="/:dir" component={DirectoryLayout}>
|
||||
<Route path="/" component={() => <Navigate href="session" />} />
|
||||
<Route
|
||||
path="/session/:id?"
|
||||
component={(p) => (
|
||||
<Show when={p.params.id || true} keyed>
|
||||
<SessionProvider>
|
||||
<Session />
|
||||
</SessionProvider>
|
||||
</Show>
|
||||
)}
|
||||
/>
|
||||
</Route>
|
||||
</Router>
|
||||
</MetaProvider>
|
||||
</NotificationProvider>
|
||||
</LayoutProvider>
|
||||
</GlobalSyncProvider>
|
||||
</GlobalSDKProvider>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { Session } from "@opencode-ai/sdk/v2/client"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Mark } from "@opencode-ai/ui/logo"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { base64Decode } from "@opencode-ai/util/encode"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { A, useParams } from "@solidjs/router"
|
||||
import { createMemo, Show } from "solid-js"
|
||||
|
||||
export function Header(props: {
|
||||
navigateToProject: (directory: string) => void
|
||||
navigateToSession: (session: Session | undefined) => void
|
||||
}) {
|
||||
const globalSync = useGlobalSync()
|
||||
const layout = useLayout()
|
||||
const params = useParams()
|
||||
const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
|
||||
const store = createMemo(() => globalSync.child(currentDirectory())[0])
|
||||
const sessions = createMemo(() => store().session ?? [])
|
||||
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
|
||||
|
||||
return (
|
||||
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
|
||||
<A
|
||||
href="/"
|
||||
classList={{
|
||||
"w-12 shrink-0 px-4 py-3.5": true,
|
||||
"flex items-center justify-start self-stretch": true,
|
||||
"border-r border-border-weak-base": true,
|
||||
}}
|
||||
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<Mark class="shrink-0" />
|
||||
</A>
|
||||
<div class="pl-4 px-6 flex items-center justify-between gap-4 w-full">
|
||||
<Show when={params.dir && layout.projects.list().length > 0}>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Select
|
||||
options={layout.projects.list().map((project) => project.worktree)}
|
||||
current={currentDirectory()}
|
||||
label={(x) => getFilename(x)}
|
||||
onSelect={(x) => (x ? props.navigateToProject(x) : undefined)}
|
||||
class="text-14-regular text-text-base"
|
||||
variant="ghost"
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
{(i) => (
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="folder" size="small" />
|
||||
<div class="text-text-strong">{getFilename(i)}</div>
|
||||
</div>
|
||||
)}
|
||||
</Select>
|
||||
<div class="text-text-weaker">/</div>
|
||||
<Select
|
||||
options={sessions()}
|
||||
current={currentSession()}
|
||||
placeholder="New session"
|
||||
label={(x) => x.title}
|
||||
value={(x) => x.id}
|
||||
onSelect={props.navigateToSession}
|
||||
class="text-14-regular text-text-base max-w-md"
|
||||
variant="ghost"
|
||||
/>
|
||||
</div>
|
||||
<Show when={currentSession()}>
|
||||
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
|
||||
New session
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<Tooltip
|
||||
class="shrink-0"
|
||||
value={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Toggle terminal</span>
|
||||
<span class="text-icon-base text-12-medium">Ctrl `</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
size="small"
|
||||
name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
|
||||
class="group-hover/terminal-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name="layout-bottom-partial"
|
||||
class="hidden group-hover/terminal-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
|
||||
class="hidden group-active/terminal-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
|
@ -55,45 +55,20 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
|
|||
const globalSDK = useGlobalSDK()
|
||||
const [globalStore, setGlobalStore] = createStore<{
|
||||
ready: boolean
|
||||
path: Path
|
||||
project: Project[]
|
||||
provider: ProviderListResponse
|
||||
provider_auth: ProviderAuthResponse
|
||||
children: Record<string, State>
|
||||
}>({
|
||||
ready: false,
|
||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||
project: [],
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
provider_auth: {},
|
||||
children: {},
|
||||
})
|
||||
|
||||
async function bootstrapInstance(directory: string) {
|
||||
const [store, setStore] = child(directory)
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: globalSDK.url,
|
||||
directory,
|
||||
})
|
||||
const load = {
|
||||
project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
|
||||
provider: () => sdk.provider.list().then((x) => setStore("provider", x.data!)),
|
||||
path: () => sdk.path.get().then((x) => setStore("path", x.data!)),
|
||||
agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
|
||||
session: () =>
|
||||
sdk.session.list().then((x) => {
|
||||
const sessions = (x.data ?? [])
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
.slice(0, store.limit)
|
||||
setStore("session", sessions)
|
||||
}),
|
||||
status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)),
|
||||
config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
|
||||
changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)),
|
||||
node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
|
||||
}
|
||||
await Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
|
||||
}
|
||||
|
||||
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
|
||||
function child(directory: string) {
|
||||
if (!children[directory]) {
|
||||
|
|
@ -120,6 +95,38 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
|
|||
return children[directory]
|
||||
}
|
||||
|
||||
async function loadSessions(directory: string) {
|
||||
globalSDK.client.session.list({ directory }).then((x) => {
|
||||
const sessions = (x.data ?? [])
|
||||
.slice()
|
||||
.filter((s) => !s.time.archived)
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
.slice(0, 5)
|
||||
const [, setStore] = child(directory)
|
||||
setStore("session", sessions)
|
||||
})
|
||||
}
|
||||
|
||||
async function bootstrapInstance(directory: string) {
|
||||
const [, setStore] = child(directory)
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: globalSDK.url,
|
||||
directory,
|
||||
})
|
||||
const load = {
|
||||
project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
|
||||
provider: () => sdk.provider.list().then((x) => setStore("provider", x.data!)),
|
||||
path: () => sdk.path.get().then((x) => setStore("path", x.data!)),
|
||||
agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
|
||||
session: () => loadSessions(directory),
|
||||
status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)),
|
||||
config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
|
||||
changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)),
|
||||
node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
|
||||
}
|
||||
await Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
|
||||
}
|
||||
|
||||
globalSDK.event.listen((e) => {
|
||||
const directory = e.name
|
||||
const event = e.details
|
||||
|
|
@ -156,6 +163,17 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
|
|||
}
|
||||
case "session.updated": {
|
||||
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
||||
if (event.properties.info.time.archived) {
|
||||
if (result.found) {
|
||||
setStore(
|
||||
"session",
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 1)
|
||||
}),
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
if (result.found) {
|
||||
setStore("session", result.index, reconcile(event.properties.info))
|
||||
break
|
||||
|
|
@ -224,6 +242,9 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
|
|||
|
||||
async function bootstrap() {
|
||||
return Promise.all([
|
||||
globalSDK.client.path.get().then((x) => {
|
||||
setGlobalStore("path", x.data!)
|
||||
}),
|
||||
globalSDK.client.project.list().then(async (x) => {
|
||||
setGlobalStore(
|
||||
"project",
|
||||
|
|
@ -252,6 +273,9 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
|
|||
},
|
||||
child,
|
||||
bootstrap,
|
||||
project: {
|
||||
loadSessions,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,15 +7,10 @@ import { useGlobalSDK } from "./global-sdk"
|
|||
import { Project } from "@opencode-ai/sdk/v2"
|
||||
|
||||
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
||||
|
||||
export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number]
|
||||
|
||||
export function isAvatarColorKey(value: string): value is AvatarColorKey {
|
||||
return AVATAR_COLOR_KEYS.includes(value as AvatarColorKey)
|
||||
}
|
||||
|
||||
export function getAvatarColors(key?: string) {
|
||||
if (key && isAvatarColorKey(key)) {
|
||||
if (key && AVATAR_COLOR_KEYS.includes(key as AvatarColorKey)) {
|
||||
return {
|
||||
background: `var(--avatar-background-${key})`,
|
||||
foreground: `var(--avatar-text-${key})`,
|
||||
|
|
@ -50,7 +45,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||
},
|
||||
}),
|
||||
{
|
||||
name: "default-layout.v7",
|
||||
name: "layout.v1",
|
||||
},
|
||||
)
|
||||
const [ephemeral, setEphemeral] = createStore<{
|
||||
|
|
@ -97,21 +92,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||
const enriched = createMemo(() => store.projects.flatMap(enrich))
|
||||
const list = createMemo(() => enriched().flatMap(colorize))
|
||||
|
||||
async function loadProjectSessions(directory: string) {
|
||||
const [, setStore] = globalSync.child(directory)
|
||||
globalSdk.client.session.list({ directory }).then((x) => {
|
||||
const sessions = (x.data ?? [])
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
.slice(0, 5)
|
||||
setStore("session", sessions)
|
||||
})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
Promise.all(
|
||||
store.projects.map((project) => {
|
||||
return loadProjectSessions(project.worktree)
|
||||
return globalSync.project.loadSessions(project.worktree)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
@ -121,7 +105,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||
list,
|
||||
open(directory: string) {
|
||||
if (store.projects.find((x) => x.worktree === directory)) return
|
||||
loadProjectSessions(directory)
|
||||
globalSync.project.loadSessions(directory)
|
||||
setStore("projects", (x) => [{ worktree: directory, expanded: true }, ...x])
|
||||
},
|
||||
close(directory: string) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
import { createStore } from "solid-js/store"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { makePersisted } from "@solid-primitives/storage"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
import { EventSessionError } from "@opencode-ai/sdk/v2"
|
||||
import { makeAudioPlayer } from "@solid-primitives/audio"
|
||||
import idleSound from "@opencode-ai/ui/audio/staplebops-01.aac"
|
||||
|
||||
type NotificationBase = {
|
||||
directory?: string
|
||||
session?: string
|
||||
metadata?: any
|
||||
time: number
|
||||
viewed: boolean
|
||||
}
|
||||
|
||||
type TurnCompleteNotification = NotificationBase & {
|
||||
type: "turn-complete"
|
||||
}
|
||||
|
||||
type ErrorNotification = NotificationBase & {
|
||||
type: "error"
|
||||
error: EventSessionError["properties"]["error"]
|
||||
}
|
||||
|
||||
export type Notification = TurnCompleteNotification | ErrorNotification
|
||||
|
||||
export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({
|
||||
name: "Notification",
|
||||
init: () => {
|
||||
const idlePlayer = makeAudioPlayer(idleSound)
|
||||
const globalSDK = useGlobalSDK()
|
||||
|
||||
const [store, setStore] = makePersisted(
|
||||
createStore({
|
||||
list: [] as Notification[],
|
||||
}),
|
||||
{
|
||||
name: "notification.v1",
|
||||
},
|
||||
)
|
||||
|
||||
// onMount(() => {
|
||||
// const daysToKeep = 7
|
||||
// // setStore("list", (n) => n.filter((n) => !n.viewed && n.time + 1000 * 60 * 60 * 24 * daysToKeep < Date.now()))
|
||||
// })
|
||||
|
||||
globalSDK.event.listen((e) => {
|
||||
const directory = e.name
|
||||
const event = e.details
|
||||
const base = {
|
||||
directory,
|
||||
time: Date.now(),
|
||||
viewed: false,
|
||||
}
|
||||
switch (event.type) {
|
||||
case "session.idle": {
|
||||
idlePlayer.play()
|
||||
const session = event.properties.sessionID
|
||||
setStore("list", store.list.length, {
|
||||
...base,
|
||||
type: "turn-complete",
|
||||
session,
|
||||
})
|
||||
break
|
||||
}
|
||||
case "session.error": {
|
||||
const session = event.properties.sessionID ?? "global"
|
||||
// errorPlayer.play()
|
||||
setStore("list", store.list.length, {
|
||||
...base,
|
||||
type: "error",
|
||||
session,
|
||||
error: "error" in event.properties ? event.properties.error : undefined,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
session: {
|
||||
all(session: string) {
|
||||
return store.list.filter((n) => n.session === session)
|
||||
},
|
||||
unseen(session: string) {
|
||||
return store.list.filter((n) => n.session === session && !n.viewed)
|
||||
},
|
||||
markViewed(session: string) {
|
||||
setStore("list", (n) => n.session === session, "viewed", true)
|
||||
},
|
||||
},
|
||||
project: {
|
||||
all(directory: string) {
|
||||
return store.list.filter((n) => n.directory === directory)
|
||||
},
|
||||
unseen(directory: string) {
|
||||
return store.list.filter((n) => n.directory === directory && !n.viewed)
|
||||
},
|
||||
markViewed(directory: string) {
|
||||
setStore("list", (n) => n.directory === directory, "viewed", true)
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
@ -65,6 +65,15 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||
})
|
||||
},
|
||||
more: createMemo(() => store.session.length >= store.limit),
|
||||
archive: async (sessionID: string) => {
|
||||
await sdk.client.session.update({ sessionID, time: { archived: Date.now() } })
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
const match = Binary.search(draft.session, sessionID, (s) => s.id)
|
||||
if (match.found) draft.session.splice(match.index, 1)
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
absolute,
|
||||
get directory() {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { For, Match, Show, Switch } from "solid-js"
|
||||
import { createMemo, For, Match, Show, Switch } from "solid-js"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Logo } from "@opencode-ai/ui/logo"
|
||||
import { useLayout } from "@/context/layout"
|
||||
|
|
@ -14,6 +14,7 @@ export default function Home() {
|
|||
const layout = useLayout()
|
||||
const platform = usePlatform()
|
||||
const navigate = useNavigate()
|
||||
const homedir = createMemo(() => sync.data.path.home)
|
||||
|
||||
function openProject(directory: string) {
|
||||
layout.projects.open(directory)
|
||||
|
|
@ -61,7 +62,7 @@ export default function Home() {
|
|||
class="text-14-mono text-left justify-between px-3"
|
||||
onClick={() => openProject(project.worktree)}
|
||||
>
|
||||
{project.worktree}
|
||||
{project.worktree.replace(homedir(), "~")}
|
||||
<div class="text-14-regular text-text-weak">
|
||||
{DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,21 @@
|
|||
import { createEffect, createMemo, For, Match, onCleanup, onMount, ParentProps, Show, Switch, type JSX } from "solid-js"
|
||||
import {
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
For,
|
||||
Match,
|
||||
onCleanup,
|
||||
onMount,
|
||||
ParentProps,
|
||||
Show,
|
||||
Switch,
|
||||
type JSX,
|
||||
} from "solid-js"
|
||||
import { DateTime } from "luxon"
|
||||
import { A, useNavigate, useParams } from "@solidjs/router"
|
||||
import { useLayout, getAvatarColors } from "@/context/layout"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
|
||||
import { Mark } from "@opencode-ai/ui/logo"
|
||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
|
|
@ -15,7 +26,6 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
|
|||
import { Collapsible } from "@opencode-ai/ui/collapsible"
|
||||
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||
import { Session, Project, ProviderAuthMethod, ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
|
|
@ -42,6 +52,9 @@ import { TextField } from "@opencode-ai/ui/text-field"
|
|||
import { showToast, Toast } from "@opencode-ai/ui/toast"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
import { useNotification } from "@/context/notification"
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
import { Header } from "@/components/header"
|
||||
|
||||
export default function Layout(props: ParentProps) {
|
||||
const [store, setStore] = createStore({
|
||||
|
|
@ -54,10 +67,8 @@ export default function Layout(props: ParentProps) {
|
|||
const globalSync = useGlobalSync()
|
||||
const layout = useLayout()
|
||||
const platform = usePlatform()
|
||||
const notification = useNotification()
|
||||
const navigate = useNavigate()
|
||||
const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
|
||||
const sessions = createMemo(() => globalSync.child(currentDirectory())[0].session ?? [])
|
||||
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
|
||||
const providers = useProviders()
|
||||
|
||||
function navigateToProject(directory: string | undefined) {
|
||||
|
|
@ -77,9 +88,11 @@ export default function Layout(props: ParentProps) {
|
|||
}
|
||||
|
||||
function closeProject(directory: string) {
|
||||
const index = layout.projects.list().findIndex((x) => x.worktree === directory)
|
||||
const next = layout.projects.list()[index + 1]
|
||||
layout.projects.close(directory)
|
||||
// TODO: more intelligent navigation
|
||||
navigate("/")
|
||||
if (next) navigateToProject(next.worktree)
|
||||
else navigate("/")
|
||||
}
|
||||
|
||||
async function chooseProject() {
|
||||
|
|
@ -105,6 +118,7 @@ export default function Layout(props: ParentProps) {
|
|||
if (!params.dir || !params.id) return
|
||||
const directory = base64Decode(params.dir)
|
||||
setStore("lastSession", directory, params.id)
|
||||
notification.session.markViewed(params.id)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
|
|
@ -164,8 +178,51 @@ export default function Layout(props: ParentProps) {
|
|||
return <></>
|
||||
}
|
||||
|
||||
const ProjectAvatar = (props: {
|
||||
project: Project
|
||||
class?: string
|
||||
expandable?: boolean
|
||||
notify?: boolean
|
||||
}): JSX.Element => {
|
||||
const notification = useNotification()
|
||||
const notifications = createMemo(() => notification.project.unseen(props.project.worktree))
|
||||
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
|
||||
const name = createMemo(() => getFilename(props.project.worktree))
|
||||
const mask = "radial-gradient(circle 5px at calc(100% - 2px) 2px, transparent 5px, black 5.5px)"
|
||||
return (
|
||||
<div class="relative size-6 shrink-0">
|
||||
<Avatar
|
||||
fallback={name()}
|
||||
src={props.project.icon?.url}
|
||||
{...getAvatarColors(props.project.icon?.color)}
|
||||
class={`size-full ${props.class ?? ""}`}
|
||||
style={
|
||||
notifications().length > 0 && props.notify ? { "-webkit-mask-image": mask, "mask-image": mask } : undefined
|
||||
}
|
||||
/>
|
||||
<Show when={props.expandable}>
|
||||
<Icon
|
||||
name="chevron-right"
|
||||
size="large"
|
||||
class="hidden size-full items-center justify-center text-text-subtle group-hover/session:flex group-data-[expanded]/trigger:rotate-90 transition-transform duration-50"
|
||||
/>
|
||||
</Show>
|
||||
<Show when={notifications().length > 0 && props.notify}>
|
||||
<div
|
||||
classList={{
|
||||
"absolute -top-0.5 -right-0.5 size-1.5 rounded-full": true,
|
||||
"bg-icon-critical-base": hasError(),
|
||||
"bg-text-interactive-base": !hasError(),
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ProjectVisual = (props: { project: Project & { expanded: boolean }; class?: string }): JSX.Element => {
|
||||
const name = createMemo(() => getFilename(props.project.worktree))
|
||||
const current = createMemo(() => base64Decode(params.dir ?? ""))
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={layout.sidebar.opened()}>
|
||||
|
|
@ -176,14 +233,7 @@ export default function Layout(props: ParentProps) {
|
|||
class="flex items-center justify-between gap-3 w-full px-1 self-stretch h-8 border-none rounded-lg"
|
||||
>
|
||||
<div class="flex items-center gap-3 p-0 text-left min-w-0 grow">
|
||||
<div class="size-6 shrink-0">
|
||||
<Avatar
|
||||
fallback={name()}
|
||||
src={props.project.icon?.url}
|
||||
{...getAvatarColors(props.project.icon?.color)}
|
||||
class="size-full"
|
||||
/>
|
||||
</div>
|
||||
<ProjectAvatar project={props.project} />
|
||||
<span class="truncate text-14-medium text-text-strong">{name()}</span>
|
||||
</div>
|
||||
</Button>
|
||||
|
|
@ -193,17 +243,10 @@ export default function Layout(props: ParentProps) {
|
|||
variant="ghost"
|
||||
size="large"
|
||||
class="flex items-center justify-center p-0 aspect-square border-none rounded-lg"
|
||||
data-selected={props.project.worktree === currentDirectory()}
|
||||
data-selected={props.project.worktree === current()}
|
||||
onClick={() => navigateToProject(props.project.worktree)}
|
||||
>
|
||||
<div class="size-6 shrink-0">
|
||||
<Avatar
|
||||
fallback={name()}
|
||||
src={props.project.icon?.url}
|
||||
{...getAvatarColors(props.project.icon?.color)}
|
||||
class="size-full"
|
||||
/>
|
||||
</div>
|
||||
<ProjectAvatar project={props.project} notify />
|
||||
</Button>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
|
@ -211,35 +254,31 @@ export default function Layout(props: ParentProps) {
|
|||
}
|
||||
|
||||
const SortableProject = (props: { project: Project & { expanded: boolean } }): JSX.Element => {
|
||||
const notification = useNotification()
|
||||
const sortable = createSortable(props.project.worktree)
|
||||
const [projectStore] = globalSync.child(props.project.worktree)
|
||||
const slug = createMemo(() => base64Encode(props.project.worktree))
|
||||
const name = createMemo(() => getFilename(props.project.worktree))
|
||||
const [store, setStore] = globalSync.child(props.project.worktree)
|
||||
const sessions = createMemo(() => store.session ?? [])
|
||||
const [expanded, setExpanded] = createSignal(true)
|
||||
return (
|
||||
// @ts-ignore
|
||||
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
|
||||
<Switch>
|
||||
<Match when={layout.sidebar.opened()}>
|
||||
<Collapsible variant="ghost" defaultOpen class="gap-2 shrink-0">
|
||||
<Collapsible variant="ghost" defaultOpen class="gap-2 shrink-0" onOpenChange={setExpanded}>
|
||||
<Button
|
||||
as={"div"}
|
||||
variant="ghost"
|
||||
class="group/session flex items-center justify-between gap-3 w-full px-1 self-stretch h-auto border-none rounded-lg"
|
||||
>
|
||||
<Collapsible.Trigger class="group/trigger flex items-center gap-3 p-0 text-left min-w-0 grow border-none">
|
||||
<div class="size-6 shrink-0">
|
||||
<Avatar
|
||||
fallback={name()}
|
||||
src={props.project.icon?.url}
|
||||
{...getAvatarColors(props.project.icon?.color)}
|
||||
class="size-full group-hover/session:hidden"
|
||||
/>
|
||||
<Icon
|
||||
name="chevron-right"
|
||||
size="large"
|
||||
class="hidden size-full items-center justify-center text-text-subtle group-hover/session:flex group-data-[expanded]/trigger:rotate-90 transition-transform duration-50"
|
||||
/>
|
||||
</div>
|
||||
<ProjectAvatar
|
||||
project={props.project}
|
||||
class="group-hover/session:hidden"
|
||||
expandable
|
||||
notify={!expanded()}
|
||||
/>
|
||||
<span class="truncate text-14-medium text-text-strong">{name()}</span>
|
||||
</Collapsible.Trigger>
|
||||
<div class="flex invisible gap-1 items-center group-hover/session:visible has-[[data-expanded]]:visible">
|
||||
|
|
@ -260,50 +299,102 @@ export default function Layout(props: ParentProps) {
|
|||
</Button>
|
||||
<Collapsible.Content>
|
||||
<nav class="hidden @[4rem]:flex w-full flex-col gap-1.5">
|
||||
<For each={projectStore.session}>
|
||||
<For each={sessions()}>
|
||||
{(session) => {
|
||||
const updated = createMemo(() => DateTime.fromMillis(session.time.updated))
|
||||
const notifications = createMemo(() => notification.session.unseen(session.id))
|
||||
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
|
||||
async function archive(session: Session) {
|
||||
await globalSDK.client.session.update({
|
||||
directory: session.directory,
|
||||
sessionID: session.id,
|
||||
time: { archived: Date.now() },
|
||||
})
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
const match = Binary.search(draft.session, session.id, (s) => s.id)
|
||||
if (match.found) draft.session.splice(match.index, 1)
|
||||
}),
|
||||
)
|
||||
}
|
||||
return (
|
||||
<A
|
||||
data-active={session.id === params.id}
|
||||
href={`${slug()}/session/${session.id}`}
|
||||
class="group/session focus:outline-none cursor-default"
|
||||
>
|
||||
<Tooltip placement="right" value={session.title}>
|
||||
<div
|
||||
class="w-full pl-4 pr-2 py-1 rounded-md
|
||||
group-data-[active=true]/session:bg-surface-raised-base-hover
|
||||
group-hover/session:bg-surface-raised-base-hover
|
||||
group-focus/session:bg-surface-raised-base-hover"
|
||||
class="relative w-full pl-4 pr-1 py-1 rounded-md
|
||||
group-[.active]/session:bg-surface-raised-base-hover
|
||||
group-hover/session:bg-surface-raised-base-hover
|
||||
group-focus/session:bg-surface-raised-base-hover"
|
||||
>
|
||||
<div class="flex items-center self-stretch gap-6 justify-between">
|
||||
<span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
|
||||
{session.title}
|
||||
</span>
|
||||
<span class="text-12-regular text-text-weak text-right whitespace-nowrap">
|
||||
{Math.abs(updated().diffNow().as("seconds")) < 60
|
||||
? "Now"
|
||||
: updated()
|
||||
.toRelative({
|
||||
style: "short",
|
||||
unit: ["days", "hours", "minutes"],
|
||||
})
|
||||
?.replace(" ago", "")
|
||||
?.replace(/ days?/, "d")
|
||||
?.replace(" min.", "m")
|
||||
?.replace(" hr.", "h")}
|
||||
</span>
|
||||
</div>
|
||||
<div class="hidden _flex justify-between items-center self-stretch">
|
||||
<span class="text-12-regular text-text-weak">{`${session.summary?.files || "No"} file${session.summary?.files !== 1 ? "s" : ""} changed`}</span>
|
||||
<Show when={session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show>
|
||||
<div class="shrink-0 group-hover/session:hidden mr-1">
|
||||
<Switch>
|
||||
<Match when={hasError()}>
|
||||
<div class="size-1.5 mr-1.5 rounded-full bg-text-diff-delete-base" />
|
||||
</Match>
|
||||
<Match when={notifications().length > 0}>
|
||||
<div class="size-1.5 mr-1.5 rounded-full bg-text-interactive-base" />
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<span class="text-12-regular text-text-weak text-right whitespace-nowrap">
|
||||
{Math.abs(updated().diffNow().as("seconds")) < 60
|
||||
? "Now"
|
||||
: updated()
|
||||
.toRelative({
|
||||
style: "short",
|
||||
unit: ["days", "hours", "minutes"],
|
||||
})
|
||||
?.replace(" ago", "")
|
||||
?.replace(/ days?/, "d")
|
||||
?.replace(" min.", "m")
|
||||
?.replace(" hr.", "h")}
|
||||
</span>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
<div class="hidden group-hover/session:flex group-active/session:flex text-text-base gap-1">
|
||||
{/* <IconButton icon="dot-grid" variant="ghost" /> */}
|
||||
<Tooltip placement="right" value="Archive session">
|
||||
<IconButton icon="archive" variant="ghost" onClick={() => archive(session)} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={session.summary?.files}>
|
||||
<div class="flex justify-between items-center self-stretch">
|
||||
<span class="text-12-regular text-text-weak">{`${session.summary?.files || "No"} file${session.summary?.files !== 1 ? "s" : ""} changed`}</span>
|
||||
<Show when={session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</A>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
<Show when={sessions().length === 0}>
|
||||
<A href={`${slug()}/session`} class="group/session focus:outline-none cursor-default">
|
||||
<Tooltip placement="right" value="New session">
|
||||
<div
|
||||
class="relative w-full pl-4 pr-1 py-1 rounded-md
|
||||
group-[.active]/session:bg-surface-raised-base-hover
|
||||
group-hover/session:bg-surface-raised-base-hover
|
||||
group-focus/session:bg-surface-raised-base-hover"
|
||||
>
|
||||
<div class="flex items-center self-stretch gap-6 justify-between">
|
||||
<span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
|
||||
New session
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</A>
|
||||
</Show>
|
||||
</nav>
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
|
|
@ -332,93 +423,9 @@ export default function Layout(props: ParentProps) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div class="relative h-screen flex flex-col">
|
||||
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
|
||||
<A
|
||||
href="/"
|
||||
classList={{
|
||||
"w-12 shrink-0 px-4 py-3.5": true,
|
||||
"flex items-center justify-start self-stretch": true,
|
||||
"border-r border-border-weak-base": true,
|
||||
}}
|
||||
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<Mark class="shrink-0" />
|
||||
</A>
|
||||
<div class="pl-4 px-6 flex items-center justify-between gap-4 w-full">
|
||||
<Show when={params.dir && layout.projects.list().length > 0}>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Select
|
||||
options={layout.projects.list().map((project) => project.worktree)}
|
||||
current={currentDirectory()}
|
||||
label={(x) => getFilename(x)}
|
||||
onSelect={(x) => (x ? navigateToProject(x) : undefined)}
|
||||
class="text-14-regular text-text-base"
|
||||
variant="ghost"
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
{(i) => (
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="folder" size="small" />
|
||||
<div class="text-text-strong">{getFilename(i)}</div>
|
||||
</div>
|
||||
)}
|
||||
</Select>
|
||||
<div class="text-text-weaker">/</div>
|
||||
<Select
|
||||
options={sessions()}
|
||||
current={currentSession()}
|
||||
placeholder="New session"
|
||||
label={(x) => x.title}
|
||||
value={(x) => x.id}
|
||||
onSelect={navigateToSession}
|
||||
class="text-14-regular text-text-base max-w-md"
|
||||
variant="ghost"
|
||||
/>
|
||||
</div>
|
||||
<Show when={currentSession()}>
|
||||
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
|
||||
New session
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<Tooltip
|
||||
class="shrink-0"
|
||||
value={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Toggle terminal</span>
|
||||
<span class="text-icon-base text-12-medium">Ctrl `</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
size="small"
|
||||
name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
|
||||
class="group-hover/terminal-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name="layout-bottom-partial"
|
||||
class="hidden group-hover/terminal-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
|
||||
class="hidden group-active/terminal-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</header>
|
||||
<div class="h-[calc(100vh-3rem)] flex">
|
||||
<div class="relative flex-1 min-h-0 flex flex-col">
|
||||
<Header navigateToProject={navigateToProject} navigateToSession={navigateToSession} />
|
||||
<div class="flex-1 min-h-0 flex">
|
||||
<div
|
||||
classList={{
|
||||
"relative @container w-12 pb-5 shrink-0 bg-background-base": true,
|
||||
|
|
@ -501,7 +508,7 @@ export default function Layout(props: ParentProps) {
|
|||
<Button
|
||||
class="flex w-full text-left justify-start text-12-medium text-text-strong stroke-[1.5px] rounded-lg rounded-t-none shadow-none border-t border-border-weak-base pl-2.25 pb-px"
|
||||
size="large"
|
||||
icon="plus-small"
|
||||
icon="plus"
|
||||
onClick={connectProvider}
|
||||
>
|
||||
<Show when={layout.sidebar.opened()}>Connect provider</Show>
|
||||
|
|
@ -515,7 +522,7 @@ export default function Layout(props: ParentProps) {
|
|||
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg px-2"
|
||||
variant="ghost"
|
||||
size="large"
|
||||
icon="plus-small"
|
||||
icon="plus"
|
||||
onClick={connectProvider}
|
||||
>
|
||||
<Show when={layout.sidebar.opened()}>Connect provider</Show>
|
||||
|
|
|
|||
|
|
@ -675,7 +675,7 @@ export default function Page() {
|
|||
<For each={session.terminal.all()}>{(terminal) => <SortableTerminalTab terminal={terminal} />}</For>
|
||||
</SortableProvider>
|
||||
<div class="h-full flex items-center justify-center">
|
||||
<Tooltip value="Open file" class="flex items-center">
|
||||
<Tooltip value="New Terminal" class="flex items-center">
|
||||
<IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={session.terminal.new} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -50,10 +50,6 @@ declare module "sst" {
|
|||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"Enterprise": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
"GITHUB_APP_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
|
@ -94,6 +90,10 @@ declare module "sst" {
|
|||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
"Teams": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
"Web": {
|
||||
"type": "sst.cloudflare.Astro"
|
||||
"url": string
|
||||
|
|
@ -114,6 +114,10 @@ declare module "sst" {
|
|||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS5": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
|
|
|
|||
|
|
@ -50,10 +50,6 @@ declare module "sst" {
|
|||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"Enterprise": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
"GITHUB_APP_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
|
@ -94,6 +90,10 @@ declare module "sst" {
|
|||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
"Teams": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
"Web": {
|
||||
"type": "sst.cloudflare.Astro"
|
||||
"url": string
|
||||
|
|
@ -114,6 +114,10 @@ declare module "sst" {
|
|||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS5": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
|
|
|
|||
|
|
@ -914,6 +914,8 @@ export namespace ACP {
|
|||
{
|
||||
sessionID,
|
||||
directory,
|
||||
providerID: model.providerID,
|
||||
modelID: model.modelID,
|
||||
},
|
||||
{ throwOnError: true },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const DiagnosticsCommand = cmd({
|
|||
async handler(args) {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
await LSP.touchFile(args.file, true)
|
||||
await Bun.sleep(1000)
|
||||
process.stdout.write(JSON.stringify(await LSP.diagnostics(), null, 2) + EOL)
|
||||
})
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { Installation } from "@/installation"
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import { For } from "solid-js"
|
||||
import { useTheme } from "@tui/context/theme"
|
||||
|
|
@ -14,8 +13,10 @@ export function Logo() {
|
|||
<For each={LOGO_LEFT}>
|
||||
{(line, index) => (
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text fg={theme.textMuted}>{line}</text>
|
||||
<text fg={theme.text} attributes={TextAttributes.BOLD}>
|
||||
<text fg={theme.textMuted} selectable={false}>
|
||||
{line}
|
||||
</text>
|
||||
<text fg={theme.text} attributes={TextAttributes.BOLD} selectable={false}>
|
||||
{LOGO_RIGHT[index()]}
|
||||
</text>
|
||||
</box>
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export namespace Clipboard {
|
|||
const getCopyMethod = lazy(() => {
|
||||
const os = platform()
|
||||
|
||||
if (os === "darwin" && Bun.which("oascript")) {
|
||||
if (os === "darwin" && Bun.which("osascript")) {
|
||||
console.log("clipboard: using osascript")
|
||||
return async (text: string) => {
|
||||
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
||||
|
|
|
|||
|
|
@ -272,7 +272,7 @@ export namespace ProviderTransform {
|
|||
const options: Record<string, any> = {}
|
||||
|
||||
if (model.providerID === "openai" || model.api.id.includes("gpt-5")) {
|
||||
if (model.api.id.includes("5.1")) {
|
||||
if (model.api.id.includes("5.")) {
|
||||
options["reasoningEffort"] = "low"
|
||||
} else {
|
||||
options["reasoningEffort"] = "minimal"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
<meta property="og:image" content="/social-share.png" />
|
||||
<meta property="twitter:image" content="/social-share.png" />
|
||||
</head>
|
||||
<body class="antialiased overscroll-none select-none text-12-regular">
|
||||
<body class="antialiased overscroll-none select-none text-12-regular overflow-hidden">
|
||||
<script>
|
||||
;(function () {
|
||||
const savedTheme = localStorage.getItem("theme") || "oc-1"
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
})()
|
||||
</script>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<div id="root" class="flex flex-col h-screen"></div>
|
||||
<script src="/src/index.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -16,9 +16,12 @@
|
|||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-dialog": "~2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"@tauri-apps/plugin-os": "~2",
|
||||
"@tauri-apps/plugin-process": "~2",
|
||||
"@tauri-apps/plugin-shell": "~2",
|
||||
"@tauri-apps/plugin-store": "~2",
|
||||
"@tauri-apps/plugin-updater": "~2",
|
||||
"@tauri-apps/plugin-window-state": "~2",
|
||||
"solid-js": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -1256,6 +1256,16 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gethostname"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.16"
|
||||
|
|
@ -2309,6 +2319,16 @@ dependencies = [
|
|||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-location"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009"
|
||||
dependencies = [
|
||||
"objc2 0.6.3",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-text"
|
||||
version = "0.3.2"
|
||||
|
|
@ -2440,6 +2460,7 @@ checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
|
|||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"objc2 0.6.3",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
|
|
@ -2461,8 +2482,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.6.2",
|
||||
"objc2 0.6.3",
|
||||
"objc2-cloud-kit",
|
||||
"objc2-core-data",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-core-image",
|
||||
"objc2-core-location",
|
||||
"objc2-core-text",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-quartz-core 0.3.2",
|
||||
"objc2-user-notifications",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-user-notifications"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e"
|
||||
dependencies = [
|
||||
"objc2 0.6.3",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
|
|
@ -2511,9 +2551,12 @@ dependencies = [
|
|||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-os",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-store",
|
||||
"tauri-plugin-updater",
|
||||
"tauri-plugin-window-state",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
|
|
@ -2533,6 +2576,22 @@ dependencies = [
|
|||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_info"
|
||||
version = "3.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c39b5918402d564846d5aba164c09a66cc88d232179dfd3e3c619a25a268392"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"log",
|
||||
"nix",
|
||||
"objc2 0.6.3",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-ui-kit",
|
||||
"serde",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_pipe"
|
||||
version = "1.2.3"
|
||||
|
|
@ -3870,6 +3929,15 @@ dependencies = [
|
|||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sys-locale"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "6.2.2"
|
||||
|
|
@ -4144,6 +4212,24 @@ dependencies = [
|
|||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-os"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8f08346c8deb39e96f86973da0e2d76cbb933d7ac9b750f6dc4daf955a6f997"
|
||||
dependencies = [
|
||||
"gethostname",
|
||||
"log",
|
||||
"os_info",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serialize-to-javascript",
|
||||
"sys-locale",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-process"
|
||||
version = "2.3.1"
|
||||
|
|
@ -4175,6 +4261,22 @@ dependencies = [
|
|||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-store"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59a77036340a97eb5bbe1b3209c31e5f27f75e6f92a52fd9dd4b211ef08bf310"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.9.0"
|
||||
|
|
@ -4207,6 +4309,21 @@ dependencies = [
|
|||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-window-state"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.9.1"
|
||||
|
|
@ -4440,10 +4557,22 @@ dependencies = [
|
|||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"tracing",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
|
|
|
|||
|
|
@ -23,9 +23,12 @@ tauri-plugin-opener = "2"
|
|||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
tauri-plugin-process = "2"
|
||||
tauri-plugin-store = "2"
|
||||
tauri-plugin-window-state = "2"
|
||||
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = "1.48.0"
|
||||
listeners = "0.3"
|
||||
tauri-plugin-process = "2"
|
||||
tauri-plugin-os = "2"
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@
|
|||
"shell:default",
|
||||
"updater:default",
|
||||
"dialog:default",
|
||||
"process:default"
|
||||
"process:default",
|
||||
"store:default",
|
||||
"window-state:default",
|
||||
"os:default"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ use std::{
|
|||
sync::{Arc, Mutex},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tauri::{AppHandle, Manager, RunEvent, WebviewUrl, WebviewWindow};
|
||||
use tauri::{
|
||||
AppHandle, LogicalSize, Manager, Monitor, RunEvent, TitleBarStyle, WebviewUrl, WebviewWindow,
|
||||
};
|
||||
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
|
||||
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
|
@ -107,6 +109,9 @@ pub fn run() {
|
|||
let updater_enabled = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some();
|
||||
|
||||
let mut builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_window_state::Builder::new().build())
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
|
|
@ -167,12 +172,18 @@ pub fn run() {
|
|||
None
|
||||
};
|
||||
|
||||
let primary_monitor = app.primary_monitor().ok().flatten();
|
||||
let size = primary_monitor
|
||||
.map(|m| m.size().to_logical(m.scale_factor()))
|
||||
.unwrap_or(LogicalSize::new(1920, 1080));
|
||||
|
||||
let mut window_builder =
|
||||
WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into()))
|
||||
.title("OpenCode")
|
||||
.inner_size(800.0, 600.0)
|
||||
.inner_size(size.width as f64, size.height as f64)
|
||||
.decorations(true)
|
||||
.zoom_hotkeys_enabled(true)
|
||||
.title_bar_style(TitleBarStyle::Overlay)
|
||||
.initialization_script(format!(
|
||||
r#"
|
||||
window.__OPENCODE__ ??= {{}};
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { runUpdater } from "./updater"
|
|||
import { onMount } from "solid-js"
|
||||
import { open, save } from "@tauri-apps/plugin-dialog"
|
||||
import { open as shellOpen } from "@tauri-apps/plugin-shell"
|
||||
import { type as ostype } from "@tauri-apps/plugin-os"
|
||||
|
||||
const root = document.getElementById("root")
|
||||
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||
|
|
@ -54,6 +55,9 @@ render(() => {
|
|||
|
||||
return (
|
||||
<PlatformProvider value={platform}>
|
||||
{ostype() === "macos" && (
|
||||
<div class="bg-background-base border-b border-border-weak-base h-8" data-tauri-drag-region />
|
||||
)}
|
||||
<App />
|
||||
</PlatformProvider>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@
|
|||
"./styles/tailwind": "./src/styles/tailwind/index.css",
|
||||
"./icons/provider": "./src/components/provider-icons/types.ts",
|
||||
"./icons/file-type": "./src/components/file-icons/types.ts",
|
||||
"./fonts/*": "./src/assets/fonts/*"
|
||||
"./fonts/*": "./src/assets/fonts/*",
|
||||
"./audio/*": "./src/assets/audio/*"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -4,6 +4,7 @@ const icons = {
|
|||
"align-right": `<path d="M12.292 6.04167L16.2503 9.99998L12.292 13.9583M2.91699 9.99998H15.6253M17.0837 3.75V16.25" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"arrow-up": `<path fill-rule="evenodd" clip-rule="evenodd" d="M9.99991 2.24121L16.0921 8.33343L15.2083 9.21731L10.6249 4.63397V17.5001H9.37492V4.63398L4.7916 9.21731L3.90771 8.33343L9.99991 2.24121Z" fill="currentColor"/>`,
|
||||
"arrow-left": `<path d="M8.33464 4.58398L2.91797 10.0007L8.33464 15.4173M3.33464 10.0007H17.0846" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
archive: `<path d="M16.8747 6.24935H17.3747V5.74935H16.8747V6.24935ZM16.8747 16.8743V17.3743H17.3747V16.8743H16.8747ZM3.12467 16.8743H2.62467V17.3743H3.12467V16.8743ZM3.12467 6.24935V5.74935H2.62467V6.24935H3.12467ZM2.08301 2.91602V2.41602H1.58301V2.91602H2.08301ZM17.9163 2.91602H18.4163V2.41602H17.9163V2.91602ZM17.9163 6.24935V6.74935H18.4163V6.24935H17.9163ZM2.08301 6.24935H1.58301V6.74935H2.08301V6.24935ZM8.33301 9.08268H7.83301V10.0827H8.33301V9.58268V9.08268ZM11.6663 10.0827H12.1663V9.08268H11.6663V9.58268V10.0827ZM16.8747 6.24935H16.3747V16.8743H16.8747H17.3747V6.24935H16.8747ZM16.8747 16.8743V16.3743H3.12467V16.8743V17.3743H16.8747V16.8743ZM3.12467 16.8743H3.62467V6.24935H3.12467H2.62467V16.8743H3.12467ZM3.12467 6.24935V6.74935H16.8747V6.24935V5.74935H3.12467V6.24935ZM2.08301 2.91602V3.41602H17.9163V2.91602V2.41602H2.08301V2.91602ZM17.9163 2.91602H17.4163V6.24935H17.9163H18.4163V2.91602H17.9163ZM17.9163 6.24935V5.74935H2.08301V6.24935V6.74935H17.9163V6.24935ZM2.08301 6.24935H2.58301V2.91602H2.08301H1.58301V6.24935H2.08301ZM8.33301 9.58268V10.0827H11.6663V9.58268V9.08268H8.33301V9.58268Z" fill="currentColor"/>`,
|
||||
"bubble-5": `<path d="M18.3327 9.99935C18.3327 5.57227 15.0919 2.91602 9.99935 2.91602C4.90676 2.91602 1.66602 5.57227 1.66602 9.99935C1.66602 11.1487 2.45505 13.1006 2.57637 13.3939C2.58707 13.4197 2.59766 13.4434 2.60729 13.4697C2.69121 13.6987 3.04209 14.9354 1.66602 16.7674C3.51787 17.6528 5.48453 16.1973 5.48453 16.1973C6.84518 16.9193 8.46417 17.0827 9.99935 17.0827C15.0919 17.0827 18.3327 14.4264 18.3327 9.99935Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"bullet-list": `<path d="M9.58329 13.7497H17.0833M9.58329 6.24967H17.0833M6.24996 6.24967C6.24996 7.17015 5.50377 7.91634 4.58329 7.91634C3.66282 7.91634 2.91663 7.17015 2.91663 6.24967C2.91663 5.3292 3.66282 4.58301 4.58329 4.58301C5.50377 4.58301 6.24996 5.3292 6.24996 6.24967ZM6.24996 13.7497C6.24996 14.6701 5.50377 15.4163 4.58329 15.4163C3.66282 15.4163 2.91663 14.6701 2.91663 13.7497C2.91663 12.8292 3.66282 12.083 4.58329 12.083C5.50377 12.083 6.24996 12.8292 6.24996 13.7497Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"check-small": `<path d="M6.5 11.4412L8.97059 13.5L13.5 6.5" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
|
|
@ -23,6 +24,7 @@ const icons = {
|
|||
folder: `<path d="M2.08301 2.91675V16.2501H17.9163V5.41675H9.99967L8.33301 2.91675H2.08301Z" stroke="currentColor" stroke-linecap="round"/>`,
|
||||
"magnifying-glass": `<path d="M15.8332 15.8337L13.0819 13.0824M14.6143 9.39088C14.6143 12.2759 12.2755 14.6148 9.39039 14.6148C6.50532 14.6148 4.1665 12.2759 4.1665 9.39088C4.1665 6.5058 6.50532 4.16699 9.39039 4.16699C12.2755 4.16699 14.6143 6.5058 14.6143 9.39088Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"plus-small": `<path d="M9.99984 5.41699V10.0003M9.99984 10.0003V14.5837M9.99984 10.0003H5.4165M9.99984 10.0003H14.5832" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
plus: `<path d="M9.9987 2.20703V9.9987M9.9987 9.9987V17.7904M9.9987 9.9987H2.20703M9.9987 9.9987H17.7904" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"pencil-line": `<path d="M9.58301 17.9166H17.9163M17.9163 5.83325L14.1663 2.08325L2.08301 14.1666V17.9166H5.83301L17.9163 5.83325Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
mcp: `<g><path d="M0.972656 9.37176L9.5214 1.60019C10.7018 0.527151 12.6155 0.527151 13.7957 1.60019C14.9761 2.67321 14.9761 4.41295 13.7957 5.48599L7.3397 11.3552" stroke="currentColor" stroke-linecap="round"/><path d="M7.42871 11.2747L13.7957 5.48643C14.9761 4.41338 16.8898 4.41338 18.0702 5.48643L18.1147 5.52688C19.2951 6.59993 19.2951 8.33966 18.1147 9.4127L10.3831 16.4414C9.98966 16.7991 9.98966 17.379 10.3831 17.7366L11.9707 19.1799" stroke="currentColor" stroke-linecap="round"/><path d="M11.6587 3.54346L5.33619 9.29119C4.15584 10.3642 4.15584 12.1039 5.33619 13.177C6.51649 14.25 8.43019 14.25 9.61054 13.177L15.9331 7.42923" stroke="currentColor" stroke-linecap="round"/></g>`,
|
||||
glasses: `<path d="M0.416626 7.91667H1.66663M19.5833 7.91667H18.3333M11.866 7.57987C11.3165 7.26398 10.6793 7.08333 9.99996 7.08333C9.32061 7.08333 8.68344 7.26398 8.13389 7.57987M8.74996 10C8.74996 12.0711 7.07103 13.75 4.99996 13.75C2.92889 13.75 1.24996 12.0711 1.24996 10C1.24996 7.92893 2.92889 6.25 4.99996 6.25C7.07103 6.25 8.74996 7.92893 8.74996 10ZM18.75 10C18.75 12.0711 17.071 13.75 15 13.75C12.9289 13.75 11.25 12.0711 11.25 10C11.25 7.92893 12.9289 6.25 15 6.25C17.071 6.25 18.75 7.92893 18.75 10Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
|
|
@ -39,7 +41,7 @@ const icons = {
|
|||
"square-arrow-top-right": `<path d="M7.91675 2.9165H2.91675V17.0832H17.0834V12.0832M12.0834 2.9165H17.0834V7.9165M9.58342 10.4165L16.6667 3.33317" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"speech-bubble": `<path d="M18.3334 10.0003C18.3334 5.57324 15.0927 2.91699 10.0001 2.91699C4.90749 2.91699 1.66675 5.57324 1.66675 10.0003C1.66675 11.1497 2.45578 13.1016 2.5771 13.3949C2.5878 13.4207 2.59839 13.4444 2.60802 13.4706C2.69194 13.6996 3.04282 14.9364 1.66675 16.7684C3.5186 17.6538 5.48526 16.1982 5.48526 16.1982C6.84592 16.9202 8.46491 17.0837 10.0001 17.0837C15.0927 17.0837 18.3334 14.4274 18.3334 10.0003Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"folder-add-left": `<path d="M2.08333 9.58268V2.91602H8.33333L10 5.41602H17.9167V16.2493H8.75M3.75 12.0827V14.5827M3.75 14.5827V17.0827M3.75 14.5827H1.25M3.75 14.5827H6.25" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"settings-gear": `<path d="M9.99935 2.08398L17.0827 6.04227L17.0827 13.9589L9.99934 17.9172L2.91602 13.9592L2.91602 6.04225L9.99935 2.08398Z" stroke="currentColor" stroke-linecap="square"/><path d="M12.916 10.0006C12.916 11.6115 11.6102 12.9173 9.99937 12.9173C8.38854 12.9173 7.0827 11.6115 7.0827 10.0006C7.0827 8.38982 8.38854 7.08398 9.99937 7.08398C11.6102 7.08398 12.916 8.38982 12.916 10.0006Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
"settings-gear": ` <path d="M9.99999 1L18 5.49998L18 14.5001L9.99998 19L2 14.5003L2 5.49996L9.99999 1Z" stroke="currentColor" stroke-linecap="square"/><path d="M13.2941 10.0001C13.2941 11.8313 11.8193 13.3159 10 13.3159C8.18073 13.3159 6.7059 11.8313 6.7059 10.0001C6.7059 8.16879 8.18073 6.68425 10 6.68425C11.8193 6.68425 13.2941 8.16879 13.2941 10.0001Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
github: `<path d="M10.0001 1.62549C14.6042 1.62549 18.3334 5.35465 18.3334 9.95882C18.333 11.7049 17.785 13.4068 16.7666 14.8251C15.7482 16.2434 14.3107 17.3066 12.6563 17.8651C12.2397 17.9484 12.0834 17.688 12.0834 17.4692C12.0834 17.188 12.0938 16.2922 12.0938 15.1776C12.0938 14.3963 11.8334 13.8963 11.5313 13.6359C13.3855 13.4276 15.3334 12.7192 15.3334 9.52132C15.3334 8.60465 15.0105 7.86507 14.4792 7.28174C14.5626 7.0734 14.8542 6.21924 14.3959 5.0734C14.3959 5.0734 13.698 4.84424 12.1042 5.92757C11.4376 5.74007 10.7292 5.64632 10.0209 5.64632C9.31258 5.64632 8.60425 5.74007 7.93758 5.92757C6.34383 4.85465 5.64592 5.0734 5.64592 5.0734C5.18758 6.21924 5.47925 7.0734 5.56258 7.28174C5.03133 7.86507 4.70842 8.61507 4.70842 9.52132C4.70842 12.7088 6.64592 13.4276 8.50008 13.6359C8.2605 13.8442 8.04175 14.2088 7.96883 14.7505C7.48967 14.9692 6.29175 15.3234 5.54175 14.063C5.3855 13.813 4.91675 13.1984 4.2605 13.2088C3.56258 13.2192 3.97925 13.6047 4.27092 13.7609C4.62508 13.9588 5.03133 14.6984 5.12508 14.938C5.29175 15.4067 5.83342 16.3026 7.92717 15.9172C7.92717 16.6151 7.93758 17.2713 7.93758 17.4692C7.93758 17.688 7.78133 17.938 7.36467 17.8651C5.70491 17.3126 4.26126 16.2515 3.23851 14.8324C2.21576 13.4133 1.66583 11.7081 1.66675 9.95882C1.66675 5.35465 5.39592 1.62549 10.0001 1.62549Z" fill="currentColor"/>`,
|
||||
discord: `<path d="M16.0742 4.45014C14.9244 3.92097 13.7106 3.54556 12.4638 3.3335C12.2932 3.64011 12.1388 3.95557 12.0013 4.27856C10.6732 4.07738 9.32261 4.07738 7.99451 4.27856C7.85694 3.9556 7.70257 3.64014 7.53203 3.3335C6.28441 3.54735 5.06981 3.92365 3.91889 4.45291C1.63401 7.85128 1.01462 11.1652 1.32431 14.4322C2.6624 15.426 4.16009 16.1819 5.7523 16.6668C6.11082 16.1821 6.42806 15.6678 6.70066 15.1295C6.18289 14.9351 5.68315 14.6953 5.20723 14.4128C5.33249 14.3215 5.45499 14.2274 5.57336 14.136C6.95819 14.7907 8.46965 15.1302 9.99997 15.1302C11.5303 15.1302 13.0418 14.7907 14.4266 14.136C14.5463 14.2343 14.6688 14.3284 14.7927 14.4128C14.3159 14.6957 13.8152 14.9361 13.2965 15.1309C13.5688 15.669 13.8861 16.1828 14.2449 16.6668C15.8385 16.1838 17.3373 15.4283 18.6756 14.4335C19.039 10.645 18.0549 7.36145 16.0742 4.45014ZM7.09294 12.423C6.22992 12.423 5.51693 11.6357 5.51693 10.6671C5.51693 9.69852 6.20514 8.90427 7.09019 8.90427C7.97524 8.90427 8.68272 9.69852 8.66758 10.6671C8.65244 11.6357 7.97248 12.423 7.09294 12.423ZM12.907 12.423C12.0426 12.423 11.3324 11.6357 11.3324 10.6671C11.3324 9.69852 12.0206 8.90427 12.907 8.90427C13.7934 8.90427 14.4954 9.69852 14.4803 10.6671C14.4651 11.6357 13.7865 12.423 12.907 12.423Z" fill="currentColor"/>`,
|
||||
"layout-bottom": `<path d="M18.125 18.125L1.875 18.125L1.875 1.875L18.125 1.875L18.125 18.125ZM3.125 12.8308L3.125 16.875L16.875 16.875L16.875 12.8308L3.125 12.8308ZM3.125 3.125L3.125 11.5808L16.875 11.5808L16.875 3.125L3.125 3.125Z" fill="currentColor"/>`,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--border-weak-base);
|
||||
background: var(--surface-float-base);
|
||||
color: var(--text-inverted-base);
|
||||
color: var(--text-invert-base);
|
||||
box-shadow: var(--shadow-md);
|
||||
|
||||
[data-slot="toast-inner"] {
|
||||
|
|
@ -80,7 +80,8 @@
|
|||
justify-content: center;
|
||||
|
||||
[data-component="icon"] {
|
||||
color: rgba(253, 252, 252, 0.94);
|
||||
color: var(--text-invert-stronger);
|
||||
/* color: var(--icon-invert-base); */
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +94,7 @@
|
|||
}
|
||||
|
||||
[data-slot="toast-title"] {
|
||||
color: var(--text-inverted-strong);
|
||||
color: var(--text-invert-strong);
|
||||
|
||||
/* text-14-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
|
|
@ -107,7 +108,8 @@
|
|||
}
|
||||
|
||||
[data-slot="toast-description"] {
|
||||
color: var(--text-inverted-base);
|
||||
color: var(--text-invert-base);
|
||||
text-wrap-style: pretty;
|
||||
|
||||
/* text-14-regular */
|
||||
font-family: var(--font-family-sans);
|
||||
|
|
@ -132,7 +134,7 @@
|
|||
padding: 0;
|
||||
cursor: pointer;
|
||||
|
||||
color: var(--text-inverted-strong);
|
||||
color: var(--text-invert-strong);
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-medium);
|
||||
|
|
@ -144,7 +146,7 @@
|
|||
}
|
||||
|
||||
&:last-child {
|
||||
color: var(--text-inverted-weak);
|
||||
color: var(--text-invert-weak);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
--font-family-mono: "Berkeley Mono", "Berkeley Mono Fallback";
|
||||
--font-family-mono--font-feature-settings: "ss01" 1;
|
||||
|
||||
--font-size-small: 12px;
|
||||
--font-size-small: 13px;
|
||||
--font-size-base: 14px;
|
||||
--font-size-large: 16px;
|
||||
--font-size-x-large: 20px;
|
||||
|
|
@ -122,7 +122,7 @@
|
|||
--surface-diff-hidden-weaker: var(--blue-light-1);
|
||||
--surface-diff-hidden-strong: var(--blue-light-5);
|
||||
--surface-diff-hidden-stronger: var(--blue-light-9);
|
||||
--surface-diff-add-base: var(--mint-light-3);
|
||||
--surface-diff-add-base: #dafbe0;
|
||||
--surface-diff-add-weak: var(--mint-light-2);
|
||||
--surface-diff-add-weaker: var(--mint-light-1);
|
||||
--surface-diff-add-strong: var(--mint-light-5);
|
||||
|
|
@ -265,22 +265,22 @@
|
|||
--icon-diff-add-active: var(--mint-light-12);
|
||||
--icon-diff-delete-base: var(--ember-light-10);
|
||||
--icon-diff-delete-hover: var(--ember-light-11);
|
||||
--syntax-comment: var(--text-weaker);
|
||||
--syntax-comment: var(--text-weak);
|
||||
--syntax-regexp: var(--text-base);
|
||||
--syntax-string: #007663;
|
||||
--syntax-string: #006656;
|
||||
--syntax-keyword: var(--text-weak);
|
||||
--syntax-primitive: #fb7f51;
|
||||
--syntax-operator: var(--text-weak);
|
||||
--syntax-primitive: #fb4804;
|
||||
--syntax-operator: var(--text-base);
|
||||
--syntax-variable: var(--text-strong);
|
||||
--syntax-property: #ec6cc8;
|
||||
--syntax-type: #738400;
|
||||
--syntax-constant: #00b2b9;
|
||||
--syntax-punctuation: var(--text-weaker);
|
||||
--syntax-property: #ed6dc8;
|
||||
--syntax-type: #596600;
|
||||
--syntax-constant: #007b80;
|
||||
--syntax-punctuation: var(--text-base);
|
||||
--syntax-object: var(--text-strong);
|
||||
--syntax-success: var(--apple-light-10);
|
||||
--syntax-warning: var(--amber-light-10);
|
||||
--syntax-critical: var(--ember-light-9);
|
||||
--syntax-info: #0091a7;
|
||||
--syntax-critical: var(--ember-light-10);
|
||||
--syntax-info: #0092a8;
|
||||
--syntax-diff-add: var(--mint-light-11);
|
||||
--syntax-diff-delete: var(--ember-light-11);
|
||||
--syntax-diff-unknown: #ff0000;
|
||||
|
|
@ -521,7 +521,7 @@
|
|||
--icon-diff-add-active: var(--mint-dark-11);
|
||||
--icon-diff-delete-base: var(--ember-dark-9);
|
||||
--icon-diff-delete-hover: var(--ember-dark-10);
|
||||
--syntax-comment: var(--text-weaker);
|
||||
--syntax-comment: var(--text-weak);
|
||||
--syntax-regexp: var(--text-base);
|
||||
--syntax-string: #00ceb9;
|
||||
--syntax-keyword: var(--text-weak);
|
||||
|
|
@ -531,7 +531,7 @@
|
|||
--syntax-property: #ff9ae2;
|
||||
--syntax-type: #ecf58c;
|
||||
--syntax-constant: #93e9f6;
|
||||
--syntax-punctuation: var(--text-weaker);
|
||||
--syntax-punctuation: var(--text-weak);
|
||||
--syntax-object: var(--text-strong);
|
||||
--syntax-success: var(--apple-dark-10);
|
||||
--syntax-warning: var(--amber-dark-10);
|
||||
|
|
@ -772,7 +772,7 @@ html[data-theme="oc-2-paper"] {
|
|||
--icon-diff-add-active: var(--mint-light-12);
|
||||
--icon-diff-delete-base: var(--ember-light-10);
|
||||
--icon-diff-delete-hover: var(--ember-light-11);
|
||||
--syntax-comment: var(--text-weaker);
|
||||
--syntax-comment: var(--text-weak);
|
||||
--syntax-regexp: var(--text-base);
|
||||
--syntax-string: #007663;
|
||||
--syntax-keyword: var(--text-weak);
|
||||
|
|
@ -782,7 +782,7 @@ html[data-theme="oc-2-paper"] {
|
|||
--syntax-property: #ec6cc8;
|
||||
--syntax-type: #738400;
|
||||
--syntax-constant: #00b2b9;
|
||||
--syntax-punctuation: var(--text-weaker);
|
||||
--syntax-punctuation: var(--text-weak);
|
||||
--syntax-object: var(--text-strong);
|
||||
--syntax-success: var(--apple-light-10);
|
||||
--syntax-warning: var(--amber-light-10);
|
||||
|
|
@ -1008,7 +1008,7 @@ html[data-theme="oc-2-paper"] {
|
|||
--icon-diff-add-active: var(--mint-light-12);
|
||||
--icon-diff-delete-base: var(--ember-light-10);
|
||||
--icon-diff-delete-hover: var(--ember-light-11);
|
||||
--syntax-comment: var(--text-weaker);
|
||||
--syntax-comment: var(--text-weak);
|
||||
--syntax-regexp: var(--text-base);
|
||||
--syntax-string: #007663;
|
||||
--syntax-keyword: var(--text-weak);
|
||||
|
|
@ -1018,7 +1018,7 @@ html[data-theme="oc-2-paper"] {
|
|||
--syntax-property: #ec6cc8;
|
||||
--syntax-type: #738400;
|
||||
--syntax-constant: #00b2b9;
|
||||
--syntax-punctuation: var(--text-weaker);
|
||||
--syntax-punctuation: var(--text-weak);
|
||||
--syntax-object: var(--text-strong);
|
||||
--syntax-success: var(--apple-light-10);
|
||||
--syntax-warning: var(--amber-light-10);
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ You can also access our models through the following API endpoints.
|
|||
|
||||
| Model | Model ID | Endpoint | AI SDK Package |
|
||||
| ----------------- | ----------------- | ------------------------------------------------ | --------------------------- |
|
||||
| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
|
|
@ -122,6 +123,7 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
|
|||
| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 |
|
||||
| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
|
||||
| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
|
||||
| GPT 5.2 | $1.75 | $14.00 | $0.175 | - |
|
||||
| GPT 5.1 | $1.07 | $8.50 | $0.107 | - |
|
||||
| GPT 5.1 Codex | $1.07 | $8.50 | $0.107 | - |
|
||||
| GPT 5.1 Codex Max | $1.25 | $10.00 | $0.125 | - |
|
||||
|
|
|
|||
|
|
@ -65,10 +65,6 @@ declare module "sst" {
|
|||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"Enterprise": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
"EnterpriseStorage": {
|
||||
"name": string
|
||||
"type": "sst.cloudflare.Bucket"
|
||||
|
|
@ -120,6 +116,10 @@ declare module "sst" {
|
|||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
"Teams": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
"Web": {
|
||||
"type": "sst.cloudflare.Astro"
|
||||
"url": string
|
||||
|
|
@ -140,6 +140,10 @@ declare module "sst" {
|
|||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS5": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZenData": {
|
||||
"name": string
|
||||
"type": "sst.cloudflare.Bucket"
|
||||
|
|
|
|||
Loading…
Reference in New Issue