chore: format code

This commit is contained in:
GitHub Action
2025-11-08 01:59:02 +00:00
parent 16357e8041
commit 34ff87d504
182 changed files with 940 additions and 3646 deletions

View File

@@ -171,9 +171,7 @@ try {
const summary = await summarize(response) const summary = await summarize(response)
await pushToLocalBranch(summary) await pushToLocalBranch(summary)
} }
const hasShared = prData.comments.nodes.some((c) => const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${useShareUrl()}/s/${shareId}`))
c.body.includes(`${useShareUrl()}/s/${shareId}`),
)
await updateComment(`${response}${footer({ image: !hasShared })}`) await updateComment(`${response}${footer({ image: !hasShared })}`)
} }
// Fork PR // Fork PR
@@ -185,9 +183,7 @@ try {
const summary = await summarize(response) const summary = await summarize(response)
await pushToForkBranch(summary, prData) await pushToForkBranch(summary, prData)
} }
const hasShared = prData.comments.nodes.some((c) => const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${useShareUrl()}/s/${shareId}`))
c.body.includes(`${useShareUrl()}/s/${shareId}`),
)
await updateComment(`${response}${footer({ image: !hasShared })}`) await updateComment(`${response}${footer({ image: !hasShared })}`)
} }
} }
@@ -368,9 +364,7 @@ async function getAccessToken() {
if (!response.ok) { if (!response.ok) {
const responseJson = (await response.json()) as { error?: string } const responseJson = (await response.json()) as { error?: string }
throw new Error( throw new Error(`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson.error}`)
`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson.error}`,
)
} }
const responseJson = (await response.json()) as { token: string } const responseJson = (await response.json()) as { token: string }
@@ -411,12 +405,8 @@ async function getUserPrompt() {
// ie. <img alt="Image" src="https://github.com/user-attachments/assets/xxxx" /> // ie. <img alt="Image" src="https://github.com/user-attachments/assets/xxxx" />
// ie. [api.json](https://github.com/user-attachments/files/21433810/api.json) // ie. [api.json](https://github.com/user-attachments/files/21433810/api.json)
// ie. ![Image](https://github.com/user-attachments/assets/xxxx) // ie. ![Image](https://github.com/user-attachments/assets/xxxx)
const mdMatches = prompt.matchAll( const mdMatches = prompt.matchAll(/!?\[.*?\]\((https:\/\/github\.com\/user-attachments\/[^)]+)\)/gi)
/!?\[.*?\]\((https:\/\/github\.com\/user-attachments\/[^)]+)\)/gi, const tagMatches = prompt.matchAll(/<img .*?src="(https:\/\/github\.com\/user-attachments\/[^"]+)" \/>/gi)
)
const tagMatches = prompt.matchAll(
/<img .*?src="(https:\/\/github\.com\/user-attachments\/[^"]+)" \/>/gi,
)
const matches = [...mdMatches, ...tagMatches].sort((a, b) => a.index - b.index) const matches = [...mdMatches, ...tagMatches].sort((a, b) => a.index - b.index)
console.log("Images", JSON.stringify(matches, null, 2)) console.log("Images", JSON.stringify(matches, null, 2))
@@ -443,8 +433,7 @@ async function getUserPrompt() {
// Replace img tag with file path, ie. @image.png // Replace img tag with file path, ie. @image.png
const replacement = `@${filename}` const replacement = `@${filename}`
prompt = prompt = prompt.slice(0, start + offset) + replacement + prompt.slice(start + offset + tag.length)
prompt.slice(0, start + offset) + replacement + prompt.slice(start + offset + tag.length)
offset += replacement.length - tag.length offset += replacement.length - tag.length
const contentType = res.headers.get("content-type") const contentType = res.headers.get("content-type")
@@ -512,12 +501,7 @@ async function subscribeSessionEvents() {
? JSON.stringify(part.state.input) ? JSON.stringify(part.state.input)
: "Unknown" : "Unknown"
console.log() console.log()
console.log( console.log(color + `|`, "\x1b[0m\x1b[2m" + ` ${tool.padEnd(7, " ")}`, "", "\x1b[0m" + title)
color + `|`,
"\x1b[0m\x1b[2m" + ` ${tool.padEnd(7, " ")}`,
"",
"\x1b[0m" + title,
)
} }
if (part.type === "text") { if (part.type === "text") {
@@ -729,8 +713,7 @@ async function assertPermissions() {
throw new Error(`Failed to check permissions for user ${actor}: ${error}`) throw new Error(`Failed to check permissions for user ${actor}: ${error}`)
} }
if (!["admin", "write"].includes(permission)) if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`)
throw new Error(`User ${actor} does not have write permissions`)
} }
async function updateComment(body: string) { async function updateComment(body: string) {
@@ -774,9 +757,7 @@ function footer(opts?: { image?: boolean }) {
return `<a href="${useShareUrl()}/s/${shareId}"><img width="200" alt="${titleAlt}" src="https://social-cards.sst.dev/opencode-share/${title64}.png?model=${providerID}/${modelID}&version=${session.version}&id=${shareId}" /></a>\n` return `<a href="${useShareUrl()}/s/${shareId}"><img width="200" alt="${titleAlt}" src="https://social-cards.sst.dev/opencode-share/${title64}.png?model=${providerID}/${modelID}&version=${session.version}&id=${shareId}" /></a>\n`
})() })()
const shareUrl = shareId const shareUrl = shareId ? `[opencode session](${useShareUrl()}/s/${shareId})&nbsp;&nbsp;|&nbsp;&nbsp;` : ""
? `[opencode session](${useShareUrl()}/s/${shareId})&nbsp;&nbsp;|&nbsp;&nbsp;`
: ""
return `\n\n${image}${shareUrl}[github run](${useEnvRunUrl()})` return `\n\n${image}${shareUrl}[github run](${useEnvRunUrl()})`
} }
@@ -959,13 +940,9 @@ function buildPromptDataForPR(pr: GitHubPullRequest) {
}) })
.map((c) => `- ${c.author.login} at ${c.createdAt}: ${c.body}`) .map((c) => `- ${c.author.login} at ${c.createdAt}: ${c.body}`)
const files = (pr.files.nodes || []).map( const files = (pr.files.nodes || []).map((f) => `- ${f.path} (${f.changeType}) +${f.additions}/-${f.deletions}`)
(f) => `- ${f.path} (${f.changeType}) +${f.additions}/-${f.deletions}`,
)
const reviewData = (pr.reviews.nodes || []).map((r) => { const reviewData = (pr.reviews.nodes || []).map((r) => {
const comments = (r.comments.nodes || []).map( const comments = (r.comments.nodes || []).map((c) => ` - ${c.path}:${c.line ?? "?"}: ${c.body}`)
(c) => ` - ${c.path}:${c.line ?? "?"}: ${c.body}`,
)
return [ return [
`- ${r.author.login} at ${r.submittedAt}:`, `- ${r.author.login} at ${r.submittedAt}:`,
` - Review body: ${r.body}`, ` - Review body: ${r.body}`,
@@ -987,15 +964,9 @@ function buildPromptDataForPR(pr: GitHubPullRequest) {
`Deletions: ${pr.deletions}`, `Deletions: ${pr.deletions}`,
`Total Commits: ${pr.commits.totalCount}`, `Total Commits: ${pr.commits.totalCount}`,
`Changed Files: ${pr.files.nodes.length} files`, `Changed Files: ${pr.files.nodes.length} files`,
...(comments.length > 0 ...(comments.length > 0 ? ["<pull_request_comments>", ...comments, "</pull_request_comments>"] : []),
? ["<pull_request_comments>", ...comments, "</pull_request_comments>"] ...(files.length > 0 ? ["<pull_request_changed_files>", ...files, "</pull_request_changed_files>"] : []),
: []), ...(reviewData.length > 0 ? ["<pull_request_reviews>", ...reviewData, "</pull_request_reviews>"] : []),
...(files.length > 0
? ["<pull_request_changed_files>", ...files, "</pull_request_changed_files>"]
: []),
...(reviewData.length > 0
? ["<pull_request_reviews>", ...reviewData, "</pull_request_reviews>"]
: []),
"</pull_request>", "</pull_request>",
].join("\n") ].join("\n")
} }

View File

@@ -61,13 +61,7 @@ export const auth = new sst.cloudflare.Worker("AuthApi", {
domain: `auth.${domain}`, domain: `auth.${domain}`,
handler: "packages/console/function/src/auth.ts", handler: "packages/console/function/src/auth.ts",
url: true, url: true,
link: [ link: [database, authStorage, GITHUB_CLIENT_ID_CONSOLE, GITHUB_CLIENT_SECRET_CONSOLE, GOOGLE_CLIENT_ID],
database,
authStorage,
GITHUB_CLIENT_ID_CONSOLE,
GITHUB_CLIENT_SECRET_CONSOLE,
GOOGLE_CLIENT_ID,
],
}) })
//////////////// ////////////////

View File

@@ -12,10 +12,7 @@ export default function App() {
root={(props) => ( root={(props) => (
<MetaProvider> <MetaProvider>
<Title>opencode</Title> <Title>opencode</Title>
<Meta <Meta name="description" content="OpenCode - The AI coding agent built for the terminal." />
name="description"
content="OpenCode - The AI coding agent built for the terminal."
/>
<Suspense>{props.children}</Suspense> <Suspense>{props.children}</Suspense>
</MetaProvider> </MetaProvider>
)} )}

View File

@@ -13,10 +13,7 @@ export function Faq(props: ParentProps & { question: string }) {
fill="currentColor" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path d="M12.5 11.5H19V12.5H12.5V19H11.5V12.5H5V11.5H11.5V5H12.5V11.5Z" fill="currentColor" />
d="M12.5 11.5H19V12.5H12.5V19H11.5V12.5H5V11.5H11.5V5H12.5V11.5Z"
fill="currentColor"
/>
</svg> </svg>
<svg <svg
data-slot="faq-icon-minus" data-slot="faq-icon-minus"

View File

@@ -9,23 +9,10 @@ export function IconLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
<path d="M13.7124 9.14333V4.5719H18.2838V9.14333H13.7124Z" fill="currentColor" /> <path d="M13.7124 9.14333V4.5719H18.2838V9.14333H13.7124Z" fill="currentColor" />
<path d="M13.7124 13.7136V9.14221H18.2838V13.7136H13.7124Z" fill="currentColor" /> <path d="M13.7124 13.7136V9.14221H18.2838V13.7136H13.7124Z" fill="currentColor" />
<path d="M0 18.2857V13.7142H4.57143V18.2857H0Z" fill="currentColor" fill-opacity="0.2" /> <path d="M0 18.2857V13.7142H4.57143V18.2857H0Z" fill="currentColor" fill-opacity="0.2" />
<rect <rect width="4.57143" height="4.57143" transform="translate(4.57178 13.7141)" fill="currentColor" />
width="4.57143" <path d="M4.57178 18.2855V13.7141H9.14321V18.2855H4.57178Z" fill="currentColor" fill-opacity="0.2" />
height="4.57143"
transform="translate(4.57178 13.7141)"
fill="currentColor"
/>
<path
d="M4.57178 18.2855V13.7141H9.14321V18.2855H4.57178Z"
fill="currentColor"
fill-opacity="0.2"
/>
<path d="M9.1438 18.2855V13.7141H13.7152V18.2855H9.1438Z" fill="currentColor" /> <path d="M9.1438 18.2855V13.7141H13.7152V18.2855H9.1438Z" fill="currentColor" />
<path <path d="M13.7156 18.2855V13.7141H18.287V18.2855H13.7156Z" fill="currentColor" fill-opacity="0.2" />
d="M13.7156 18.2855V13.7141H18.287V18.2855H13.7156Z"
fill="currentColor"
fill-opacity="0.2"
/>
<rect width="4.57143" height="4.57143" transform="translate(0 18.2859)" fill="currentColor" /> <rect width="4.57143" height="4.57143" transform="translate(0 18.2859)" fill="currentColor" />
<path d="M0 22.8572V18.2858H4.57143V22.8572H0Z" fill="currentColor" fill-opacity="0.2" /> <path d="M0 22.8572V18.2858H4.57143V22.8572H0Z" fill="currentColor" fill-opacity="0.2" />
<rect <rect
@@ -36,16 +23,8 @@ export function IconLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
fill-opacity="0.2" fill-opacity="0.2"
/> />
<path d="M4.57178 22.8573V18.2859H9.14321V22.8573H4.57178Z" fill="currentColor" /> <path d="M4.57178 22.8573V18.2859H9.14321V22.8573H4.57178Z" fill="currentColor" />
<path <path d="M9.1438 22.8573V18.2859H13.7152V22.8573H9.1438Z" fill="currentColor" fill-opacity="0.2" />
d="M9.1438 22.8573V18.2859H13.7152V22.8573H9.1438Z" <path d="M13.7156 22.8573V18.2859H18.287V22.8573H13.7156Z" fill="currentColor" fill-opacity="0.2" />
fill="currentColor"
fill-opacity="0.2"
/>
<path
d="M13.7156 22.8573V18.2859H18.287V22.8573H13.7156Z"
fill="currentColor"
fill-opacity="0.2"
/>
<path d="M0 27.4292V22.8578H4.57143V27.4292H0Z" fill="currentColor" /> <path d="M0 27.4292V22.8578H4.57143V27.4292H0Z" fill="currentColor" />
<path d="M4.57178 27.4292V22.8578H9.14321V27.4292H4.57178Z" fill="currentColor" /> <path d="M4.57178 27.4292V22.8578H9.14321V27.4292H4.57178Z" fill="currentColor" />
<path d="M9.1438 27.4276V22.8562H13.7152V27.4276H9.1438Z" fill="currentColor" /> <path d="M9.1438 27.4276V22.8562H13.7152V27.4276H9.1438Z" fill="currentColor" />
@@ -61,21 +40,9 @@ export function IconLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
<path d="M32.001 18.2855V13.7141H36.5724V18.2855H32.001Z" fill="currentColor" /> <path d="M32.001 18.2855V13.7141H36.5724V18.2855H32.001Z" fill="currentColor" />
<path d="M36.5698 18.2855V13.7141H41.1413V18.2855H36.5698Z" fill="currentColor" /> <path d="M36.5698 18.2855V13.7141H41.1413V18.2855H36.5698Z" fill="currentColor" />
<path d="M22.8572 22.8573V18.2859H27.4286V22.8573H22.8572Z" fill="currentColor" /> <path d="M22.8572 22.8573V18.2859H27.4286V22.8573H22.8572Z" fill="currentColor" />
<path <path d="M27.4292 22.8573V18.2859H32.0006V22.8573H27.4292Z" fill="currentColor" fill-opacity="0.2" />
d="M27.4292 22.8573V18.2859H32.0006V22.8573H27.4292Z" <path d="M32.001 22.8573V18.2859H36.5724V22.8573H32.001Z" fill="currentColor" fill-opacity="0.2" />
fill="currentColor" <path d="M36.5698 22.8573V18.2859H41.1413V22.8573H36.5698Z" fill="currentColor" fill-opacity="0.2" />
fill-opacity="0.2"
/>
<path
d="M32.001 22.8573V18.2859H36.5724V22.8573H32.001Z"
fill="currentColor"
fill-opacity="0.2"
/>
<path
d="M36.5698 22.8573V18.2859H41.1413V22.8573H36.5698Z"
fill="currentColor"
fill-opacity="0.2"
/>
<path d="M22.8572 27.4292V22.8578H27.4286V27.4292H22.8572Z" fill="currentColor" /> <path d="M22.8572 27.4292V22.8578H27.4286V27.4292H22.8572Z" fill="currentColor" />
<path d="M27.4292 27.4276V22.8562H32.0006V27.4276H27.4292Z" fill="currentColor" /> <path d="M27.4292 27.4276V22.8562H32.0006V27.4276H27.4292Z" fill="currentColor" />
<path d="M32.001 27.4276V22.8562H36.5724V27.4276H32.001Z" fill="currentColor" /> <path d="M32.001 27.4276V22.8562H36.5724V27.4276H32.001Z" fill="currentColor" />
@@ -86,40 +53,16 @@ export function IconLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
<path d="M45.7144 13.7136V9.14221H50.2858V13.7136H45.7144Z" fill="currentColor" /> <path d="M45.7144 13.7136V9.14221H50.2858V13.7136H45.7144Z" fill="currentColor" />
<path d="M59.4299 13.7152V9.1438H64.0014V13.7152H59.4299Z" fill="currentColor" /> <path d="M59.4299 13.7152V9.1438H64.0014V13.7152H59.4299Z" fill="currentColor" />
<path d="M45.7144 18.2855V13.7141H50.2858V18.2855H45.7144Z" fill="currentColor" /> <path d="M45.7144 18.2855V13.7141H50.2858V18.2855H45.7144Z" fill="currentColor" />
<path <path d="M50.2861 18.2857V13.7142H54.8576V18.2857H50.2861Z" fill="currentColor" fill-opacity="0.2" />
d="M50.2861 18.2857V13.7142H54.8576V18.2857H50.2861Z" <path d="M54.8579 18.2855V13.7141H59.4293V18.2855H54.8579Z" fill="currentColor" fill-opacity="0.2" />
fill="currentColor"
fill-opacity="0.2"
/>
<path
d="M54.8579 18.2855V13.7141H59.4293V18.2855H54.8579Z"
fill="currentColor"
fill-opacity="0.2"
/>
<path d="M59.4299 18.2855V13.7141H64.0014V18.2855H59.4299Z" fill="currentColor" /> <path d="M59.4299 18.2855V13.7141H64.0014V18.2855H59.4299Z" fill="currentColor" />
<path d="M45.7144 22.8573V18.2859H50.2858V22.8573H45.7144Z" fill="currentColor" /> <path d="M45.7144 22.8573V18.2859H50.2858V22.8573H45.7144Z" fill="currentColor" />
<path <path d="M50.2861 22.8572V18.2858H54.8576V22.8572H50.2861Z" fill="currentColor" fill-opacity="0.2" />
d="M50.2861 22.8572V18.2858H54.8576V22.8572H50.2861Z" <path d="M54.8579 22.8573V18.2859H59.4293V22.8573H54.8579Z" fill="currentColor" fill-opacity="0.2" />
fill="currentColor"
fill-opacity="0.2"
/>
<path
d="M54.8579 22.8573V18.2859H59.4293V22.8573H54.8579Z"
fill="currentColor"
fill-opacity="0.2"
/>
<path d="M59.4299 22.8573V18.2859H64.0014V22.8573H59.4299Z" fill="currentColor" /> <path d="M59.4299 22.8573V18.2859H64.0014V22.8573H59.4299Z" fill="currentColor" />
<path d="M45.7144 27.4292V22.8578H50.2858V27.4292H45.7144Z" fill="currentColor" /> <path d="M45.7144 27.4292V22.8578H50.2858V27.4292H45.7144Z" fill="currentColor" />
<path <path d="M50.2861 27.4286V22.8572H54.8576V27.4286H50.2861Z" fill="currentColor" fill-opacity="0.2" />
d="M50.2861 27.4286V22.8572H54.8576V27.4286H50.2861Z" <path d="M54.8579 27.4285V22.8571H59.4293V27.4285H54.8579Z" fill="currentColor" fill-opacity="0.2" />
fill="currentColor"
fill-opacity="0.2"
/>
<path
d="M54.8579 27.4285V22.8571H59.4293V27.4285H54.8579Z"
fill="currentColor"
fill-opacity="0.2"
/>
<path d="M59.4299 27.4292V22.8578H64.0014V27.4292H59.4299Z" fill="currentColor" /> <path d="M59.4299 27.4292V22.8578H64.0014V27.4292H59.4299Z" fill="currentColor" />
</svg> </svg>
) )
@@ -127,14 +70,7 @@ export function IconLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
export function IconCopy(props: JSX.SvgSVGAttributes<SVGSVGElement>) { export function IconCopy(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return ( return (
<svg <svg {...props} width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
{...props}
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M8.75 8.75V2.75H21.25V15.25H15.25M15.25 8.75H2.75V21.25H15.25V8.75Z" d="M8.75 8.75V2.75H21.25V15.25H15.25M15.25 8.75H2.75V21.25H15.25V8.75Z"
stroke="currentColor" stroke="currentColor"
@@ -147,20 +83,8 @@ export function IconCopy(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
export function IconCheck(props: JSX.SvgSVGAttributes<SVGSVGElement>) { export function IconCheck(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return ( return (
<svg <svg {...props} width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
{...props} <path d="M2.75 15.0938L9 20.25L21.25 3.75" stroke="#03B000" stroke-width="2" stroke-linecap="square" />
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.75 15.0938L9 20.25L21.25 3.75"
stroke="#03B000"
stroke-width="2"
stroke-linecap="square"
/>
</svg> </svg>
) )
} }
@@ -189,14 +113,7 @@ export function IconStripe(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
export function IconChevron(props: JSX.SvgSVGAttributes<SVGSVGElement>) { export function IconChevron(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return ( return (
<svg <svg {...props} width="8" height="6" viewBox="0 0 8 6" fill="none" xmlns="http://www.w3.org/2000/svg">
{...props}
width="8"
height="6"
viewBox="0 0 8 6"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
fill="currentColor" fill="currentColor"
d="M4.00024 5.04041L7.37401 1.66663L6.66691 0.959525L4.00024 3.62619L1.33357 0.959525L0.626465 1.66663L4.00024 5.04041Z" d="M4.00024 5.04041L7.37401 1.66663L6.66691 0.959525L4.00024 3.62619L1.33357 0.959525L0.626465 1.66663L4.00024 5.04041Z"
@@ -207,14 +124,7 @@ export function IconChevron(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
export function IconWorkspaceLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) { export function IconWorkspaceLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return ( return (
<svg <svg {...props} width="24" height="30" viewBox="0 0 24 30" fill="none" xmlns="http://www.w3.org/2000/svg">
{...props}
width="24"
height="30"
viewBox="0 0 24 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M18 6H6V24H18V6ZM24 30H0V0H24V30Z" fill="currentColor" /> <path d="M18 6H6V24H18V6ZM24 30H0V0H24V30Z" fill="currentColor" />
</svg> </svg>
) )
@@ -234,10 +144,7 @@ export function IconOpenAI(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
export function IconAnthropic(props: JSX.SvgSVGAttributes<SVGSVGElement>) { export function IconAnthropic(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return ( return (
<svg {...props} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg {...props} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path fill="currentColor" d="M13.7891 3.93188L20.2223 20.068H23.7502L17.317 3.93188H13.7891Z" />
fill="currentColor"
d="M13.7891 3.93188L20.2223 20.068H23.7502L17.317 3.93188H13.7891Z"
/>
<path <path
fill="currentColor" fill="currentColor"
d="M6.32538 13.6827L8.52662 8.01201L10.7279 13.6827H6.32538ZM6.68225 3.93188L0.25 20.068H3.84652L5.16202 16.6794H11.8914L13.2067 20.068H16.8033L10.371 3.93188H6.68225Z" d="M6.32538 13.6827L8.52662 8.01201L10.7279 13.6827H6.32538ZM6.68225 3.93188L0.25 20.068H3.84652L5.16202 16.6794H11.8914L13.2067 20.068H16.8033L10.371 3.93188H6.68225Z"

View File

@@ -7,10 +7,7 @@ export const github = query(async () => {
"User-Agent": "User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
} }
const apiBaseUrl = config.github.repoUrl.replace( const apiBaseUrl = config.github.repoUrl.replace("https://github.com/", "https://api.github.com/repos/")
"https://github.com/",
"https://api.github.com/repos/",
)
try { try {
const [meta, releases, contributors] = await Promise.all([ const [meta, releases, contributors] = await Promise.all([
fetch(apiBaseUrl, { headers }).then((res) => res.json()), fetch(apiBaseUrl, { headers }).then((res) => res.json()),

View File

@@ -2,9 +2,6 @@ import type { APIEvent } from "@solidjs/start/server"
import { AuthClient } from "~/context/auth" import { AuthClient } from "~/context/auth"
export async function GET(input: APIEvent) { export async function GET(input: APIEvent) {
const result = await AuthClient.authorize( const result = await AuthClient.authorize(new URL("./callback", input.request.url).toString(), "code")
new URL("./callback", input.request.url).toString(),
"code",
)
return Response.redirect(result.url, 302) return Response.redirect(result.url, 302)
} }

View File

@@ -68,13 +68,7 @@ export default function Brand() {
onClick={() => downloadFile(brandAssets, "opencode-brand-assets.zip")} onClick={() => downloadFile(brandAssets, "opencode-brand-assets.zip")}
> >
Download all assets Download all assets
<svg <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75" d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor" stroke="currentColor"
@@ -90,13 +84,7 @@ export default function Brand() {
<div data-component="actions"> <div data-component="actions">
<button onClick={() => downloadFile(logoLightPng, "opencode-logo-light.png")}> <button onClick={() => downloadFile(logoLightPng, "opencode-logo-light.png")}>
PNG PNG
<svg <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75" d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor" stroke="currentColor"
@@ -107,13 +95,7 @@ export default function Brand() {
</button> </button>
<button onClick={() => downloadFile(logoLightSvg, "opencode-logo-light.svg")}> <button onClick={() => downloadFile(logoLightSvg, "opencode-logo-light.svg")}>
SVG SVG
<svg <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75" d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor" stroke="currentColor"
@@ -129,13 +111,7 @@ export default function Brand() {
<div data-component="actions"> <div data-component="actions">
<button onClick={() => downloadFile(logoDarkPng, "opencode-logo-dark.png")}> <button onClick={() => downloadFile(logoDarkPng, "opencode-logo-dark.png")}>
PNG PNG
<svg <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75" d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor" stroke="currentColor"
@@ -146,13 +122,7 @@ export default function Brand() {
</button> </button>
<button onClick={() => downloadFile(logoDarkSvg, "opencode-logo-dark.svg")}> <button onClick={() => downloadFile(logoDarkSvg, "opencode-logo-dark.svg")}>
SVG SVG
<svg <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75" d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor" stroke="currentColor"
@@ -166,17 +136,9 @@ export default function Brand() {
<div> <div>
<img src={previewWordmarkLight} alt="OpenCode brand guidelines" /> <img src={previewWordmarkLight} alt="OpenCode brand guidelines" />
<div data-component="actions"> <div data-component="actions">
<button <button onClick={() => downloadFile(wordmarkLightPng, "opencode-wordmark-light.png")}>
onClick={() => downloadFile(wordmarkLightPng, "opencode-wordmark-light.png")}
>
PNG PNG
<svg <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75" d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor" stroke="currentColor"
@@ -185,17 +147,9 @@ export default function Brand() {
/> />
</svg> </svg>
</button> </button>
<button <button onClick={() => downloadFile(wordmarkLightSvg, "opencode-wordmark-light.svg")}>
onClick={() => downloadFile(wordmarkLightSvg, "opencode-wordmark-light.svg")}
>
SVG SVG
<svg <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75" d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor" stroke="currentColor"
@@ -209,17 +163,9 @@ export default function Brand() {
<div> <div>
<img src={previewWordmarkDark} alt="OpenCode brand guidelines" /> <img src={previewWordmarkDark} alt="OpenCode brand guidelines" />
<div data-component="actions"> <div data-component="actions">
<button <button onClick={() => downloadFile(wordmarkDarkPng, "opencode-wordmark-dark.png")}>
onClick={() => downloadFile(wordmarkDarkPng, "opencode-wordmark-dark.png")}
>
PNG PNG
<svg <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75" d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor" stroke="currentColor"
@@ -228,17 +174,9 @@ export default function Brand() {
/> />
</svg> </svg>
</button> </button>
<button <button onClick={() => downloadFile(wordmarkDarkSvg, "opencode-wordmark-dark.svg")}>
onClick={() => downloadFile(wordmarkDarkSvg, "opencode-wordmark-dark.svg")}
>
SVG SVG
<svg <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75" d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor" stroke="currentColor"
@@ -252,19 +190,9 @@ export default function Brand() {
<div> <div>
<img src={previewWordmarkSimpleLight} alt="OpenCode brand guidelines" /> <img src={previewWordmarkSimpleLight} alt="OpenCode brand guidelines" />
<div data-component="actions"> <div data-component="actions">
<button <button onClick={() => downloadFile(wordmarkSimpleLightPng, "opencode-wordmark-simple-light.png")}>
onClick={() =>
downloadFile(wordmarkSimpleLightPng, "opencode-wordmark-simple-light.png")
}
>
PNG PNG
<svg <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75" d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor" stroke="currentColor"
@@ -273,19 +201,9 @@ export default function Brand() {
/> />
</svg> </svg>
</button> </button>
<button <button onClick={() => downloadFile(wordmarkSimpleLightSvg, "opencode-wordmark-simple-light.svg")}>
onClick={() =>
downloadFile(wordmarkSimpleLightSvg, "opencode-wordmark-simple-light.svg")
}
>
SVG SVG
<svg <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75" d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor" stroke="currentColor"
@@ -299,19 +217,9 @@ export default function Brand() {
<div> <div>
<img src={previewWordmarkSimpleDark} alt="OpenCode brand guidelines" /> <img src={previewWordmarkSimpleDark} alt="OpenCode brand guidelines" />
<div data-component="actions"> <div data-component="actions">
<button <button onClick={() => downloadFile(wordmarkSimpleDarkPng, "opencode-wordmark-simple-dark.png")}>
onClick={() =>
downloadFile(wordmarkSimpleDarkPng, "opencode-wordmark-simple-dark.png")
}
>
PNG PNG
<svg <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75" d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor" stroke="currentColor"
@@ -320,19 +228,9 @@ export default function Brand() {
/> />
</svg> </svg>
</button> </button>
<button <button onClick={() => downloadFile(wordmarkSimpleDarkSvg, "opencode-wordmark-simple-dark.svg")}>
onClick={() =>
downloadFile(wordmarkSimpleDarkSvg, "opencode-wordmark-simple-dark.svg")
}
>
SVG SVG
<svg <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75" d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor" stroke="currentColor"

View File

@@ -66,39 +66,26 @@ export default function Enterprise() {
<div data-component="enterprise-column-1"> <div data-component="enterprise-column-1">
<h1>Your code is yours</h1> <h1>Your code is yours</h1>
<p> <p>
OpenCode operates securely inside your organization with no data or context stored OpenCode operates securely inside your organization with no data or context stored and no licensing
and no licensing restrictions or ownership claims. Start a trial with your team, restrictions or ownership claims. Start a trial with your team, then deploy it across your
then deploy it across your organization by integrating it with your SSO and organization by integrating it with your SSO and internal AI gateway.
internal AI gateway.
</p> </p>
<p>Let us know and how we can help.</p> <p>Let us know and how we can help.</p>
<Show when={false}> <Show when={false}>
<div data-component="testimonial"> <div data-component="testimonial">
<div data-component="quotation"> <div data-component="quotation">
<svg <svg width="20" height="17" viewBox="0 0 20 17" fill="none" xmlns="http://www.w3.org/2000/svg">
width="20"
height="17"
viewBox="0 0 20 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M19.4118 0L16.5882 9.20833H20V17H12.2353V10.0938L16 0H19.4118ZM7.17647 0L4.35294 9.20833H7.76471V17H0V10.0938L3.76471 0H7.17647Z" d="M19.4118 0L16.5882 9.20833H20V17H12.2353V10.0938L16 0H19.4118ZM7.17647 0L4.35294 9.20833H7.76471V17H0V10.0938L3.76471 0H7.17647Z"
fill="currentColor" fill="currentColor"
/> />
</svg> </svg>
</div> </div>
Thanks to OpenCode, we found a way to create software to track all our assets Thanks to OpenCode, we found a way to create software to track all our assets even the imaginary
even the imaginary ones. ones.
<div data-component="testimonial-logo"> <div data-component="testimonial-logo">
<svg <svg width="80" height="79" viewBox="0 0 80 79" fill="none" xmlns="http://www.w3.org/2000/svg">
width="80"
height="79"
viewBox="0 0 80 79"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
fill-rule="evenodd" fill-rule="evenodd"
clip-rule="evenodd" clip-rule="evenodd"
@@ -215,11 +202,7 @@ export default function Enterprise() {
</button> </button>
</form> </form>
{showSuccess() && ( {showSuccess() && <div data-component="success-message">Message sent, we'll be in touch soon.</div>}
<div data-component="success-message">
Message sent, we'll be in touch soon.
</div>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -232,31 +215,29 @@ export default function Enterprise() {
<ul> <ul>
<li> <li>
<Faq question="What is OpenCode Enterprise?"> <Faq question="What is OpenCode Enterprise?">
OpenCode Enterprise is for organizations that want to ensure that their code and OpenCode Enterprise is for organizations that want to ensure that their code and data never leaves
data never leaves their infrastructure. It can do this by using a centralized their infrastructure. It can do this by using a centralized config that integrates with your SSO and
config that integrates with your SSO and internal AI gateway. internal AI gateway.
</Faq> </Faq>
</li> </li>
<li> <li>
<Faq question="How do I get started with OpenCode Enterprise?"> <Faq question="How do I get started with OpenCode Enterprise?">
Simply start with an internal trial with your team. OpenCode by default does not Simply start with an internal trial with your team. OpenCode by default does not store your code or
store your code or context data, making it easy to get started. Then contact us to context data, making it easy to get started. Then contact us to discuss pricing and implementation
discuss pricing and implementation options. options.
</Faq> </Faq>
</li> </li>
<li> <li>
<Faq question="How does enterprise pricing work?"> <Faq question="How does enterprise pricing work?">
We offer per-seat enterprise pricing. If you have your own LLM gateway, we do not We offer per-seat enterprise pricing. If you have your own LLM gateway, we do not charge for tokens
charge for tokens used. For further details, contact us for a custom quote based used. For further details, contact us for a custom quote based on your organization's needs.
on your organization's needs.
</Faq> </Faq>
</li> </li>
<li> <li>
<Faq question="Is my data secure with OpenCode Enterprise?"> <Faq question="Is my data secure with OpenCode Enterprise?">
Yes. OpenCode does not store your code or context data. All processing happens Yes. OpenCode does not store your code or context data. All processing happens locally or through
locally or through direct API calls to your AI provider. With central config and direct API calls to your AI provider. With central config and SSO integration, your data remains
SSO integration, your data remains secure within your organization's secure within your organization's infrastructure.
infrastructure.
</Faq> </Faq>
</li> </li>
</ul> </ul>

File diff suppressed because it is too large Load Diff

View File

@@ -41,8 +41,7 @@ export async function POST(input: APIEvent) {
} }
if (body.type === "checkout.session.completed") { if (body.type === "checkout.session.completed") {
const workspaceID = body.data.object.metadata?.workspaceID const workspaceID = body.data.object.metadata?.workspaceID
const amountInCents = const amountInCents = body.data.object.metadata?.amount && parseInt(body.data.object.metadata?.amount)
body.data.object.metadata?.amount && parseInt(body.data.object.metadata?.amount)
const customerID = body.data.object.customer as string const customerID = body.data.object.customer as string
const paymentID = body.data.object.payment_intent as string const paymentID = body.data.object.payment_intent as string
const invoiceID = body.data.object.invoice as string const invoiceID = body.data.object.invoice as string
@@ -55,8 +54,7 @@ export async function POST(input: APIEvent) {
await Actor.provide("system", { workspaceID }, async () => { await Actor.provide("system", { workspaceID }, async () => {
const customer = await Billing.get() const customer = await Billing.get()
if (customer?.customerID && customer.customerID !== customerID) if (customer?.customerID && customer.customerID !== customerID) throw new Error("Customer ID mismatch")
throw new Error("Customer ID mismatch")
// set customer metadata // set customer metadata
if (!customer?.customerID) { if (!customer?.customerID) {
@@ -72,8 +70,7 @@ export async function POST(input: APIEvent) {
expand: ["payment_method"], expand: ["payment_method"],
}) })
const paymentMethod = paymentIntent.payment_method const paymentMethod = paymentIntent.payment_method
if (!paymentMethod || typeof paymentMethod === "string") if (!paymentMethod || typeof paymentMethod === "string") throw new Error("Payment method not expanded")
throw new Error("Payment method not expanded")
await Database.transaction(async (tx) => { await Database.transaction(async (tx) => {
await tx await tx
@@ -128,12 +125,7 @@ export async function POST(input: APIEvent) {
amount: PaymentTable.amount, amount: PaymentTable.amount,
}) })
.from(PaymentTable) .from(PaymentTable)
.where( .where(and(eq(PaymentTable.paymentID, paymentIntentID), eq(PaymentTable.workspaceID, workspaceID)))
and(
eq(PaymentTable.paymentID, paymentIntentID),
eq(PaymentTable.workspaceID, workspaceID),
),
)
.then((rows) => rows[0]?.amount), .then((rows) => rows[0]?.amount),
) )
if (!amount) throw new Error("Payment not found") if (!amount) throw new Error("Payment not found")
@@ -144,12 +136,7 @@ export async function POST(input: APIEvent) {
.set({ .set({
timeRefunded: new Date(body.created * 1000), timeRefunded: new Date(body.created * 1000),
}) })
.where( .where(and(eq(PaymentTable.paymentID, paymentIntentID), eq(PaymentTable.workspaceID, workspaceID)))
and(
eq(PaymentTable.paymentID, paymentIntentID),
eq(PaymentTable.workspaceID, workspaceID),
),
)
await tx await tx
.update(BillingTable) .update(BillingTable)

View File

@@ -79,19 +79,17 @@ export default function Home() {
<strong>LSP enabled</strong> Automatically loads the right LSPs for the LLM <strong>LSP enabled</strong> Automatically loads the right LSPs for the LLM
</li> </li>
<li> <li>
<strong>opencode zen</strong> A <a href="/docs/zen">curated list of models</a>{" "} <strong>opencode zen</strong> A <a href="/docs/zen">curated list of models</a> provided by opencode{" "}
provided by opencode <label>New</label> <label>New</label>
</li> </li>
<li> <li>
<strong>Multi-session</strong> Start multiple agents in parallel on the same project <strong>Multi-session</strong> Start multiple agents in parallel on the same project
</li> </li>
<li> <li>
<strong>Shareable links</strong> Share a link to any sessions for reference or to <strong>Shareable links</strong> Share a link to any sessions for reference or to debug
debug
</li> </li>
<li> <li>
<strong>Claude Pro</strong> Log in with Anthropic to use your Claude Pro or Max <strong>Claude Pro</strong> Log in with Anthropic to use your Claude Pro or Max account
account
</li> </li>
<li> <li>
<strong>Use any model</strong> Supports 75+ LLM providers through{" "} <strong>Use any model</strong> Supports 75+ LLM providers through{" "}

View File

@@ -85,10 +85,7 @@ export function WorkspacePicker() {
<Dropdown trigger={currentWorkspace()} align="left"> <Dropdown trigger={currentWorkspace()} align="left">
<For each={workspaces()}> <For each={workspaces()}>
{(workspace) => ( {(workspace) => (
<DropdownItem <DropdownItem selected={workspace.id === params.id} onClick={() => handleSelectWorkspace(workspace.id)}>
selected={workspace.id === params.id}
onClick={() => handleSelectWorkspace(workspace.id)}
>
{workspace.name || workspace.slug} {workspace.name || workspace.slug}
</DropdownItem> </DropdownItem>
)} )}
@@ -98,11 +95,7 @@ export function WorkspacePicker() {
</button> </button>
</Dropdown> </Dropdown>
<Modal <Modal open={store.showForm} onClose={() => setStore("showForm", false)} title="Create New Workspace">
open={store.showForm}
onClose={() => setStore("showForm", false)}
title="Create New Workspace"
>
<form data-slot="create-form" action={createWorkspace} method="post"> <form data-slot="create-form" action={createWorkspace} method="post">
<div data-slot="create-input-group"> <div data-slot="create-input-group">
<input <input

View File

@@ -133,8 +133,7 @@ export function BillingSection() {
<div data-slot="section-title"> <div data-slot="section-title">
<h2>Billing</h2> <h2>Billing</h2>
<p> <p>
Manage payments methods. <a href="mailto:contact@anoma.ly">Contact us</a> if you have any Manage payments methods. <a href="mailto:contact@anoma.ly">Contact us</a> if you have any questions.
questions.
</p> </p>
</div> </div>
<div data-slot="section-content"> <div data-slot="section-content">
@@ -164,32 +163,20 @@ export function BillingSection() {
placeholder="Enter amount" placeholder="Enter amount"
/> />
<div data-slot="form-actions"> <div data-slot="form-actions">
<button <button data-color="ghost" type="button" onClick={() => hideAddBalanceForm()}>
data-color="ghost"
type="button"
onClick={() => hideAddBalanceForm()}
>
Cancel Cancel
</button> </button>
<button <button
data-color="primary" data-color="primary"
type="button" type="button"
disabled={ disabled={!store.addBalanceAmount || checkoutSubmission.pending || store.checkoutRedirecting}
!store.addBalanceAmount ||
checkoutSubmission.pending ||
store.checkoutRedirecting
}
onClick={onClickCheckout} onClick={onClickCheckout}
> >
{checkoutSubmission.pending || store.checkoutRedirecting {checkoutSubmission.pending || store.checkoutRedirecting ? "Loading..." : "Add"}
? "Loading..."
: "Add"}
</button> </button>
</div> </div>
</div> </div>
<Show <Show when={checkoutSubmission.result && (checkoutSubmission.result as any).error}>
when={checkoutSubmission.result && (checkoutSubmission.result as any).error}
>
{(err: any) => <div data-slot="form-error">{err()}</div>} {(err: any) => <div data-slot="form-error">{err()}</div>}
</Show> </Show>
</div> </div>
@@ -210,10 +197,7 @@ export function BillingSection() {
<div data-slot="card-details"> <div data-slot="card-details">
<Switch> <Switch>
<Match when={billingInfo()?.paymentMethodType === "card"}> <Match when={billingInfo()?.paymentMethodType === "card"}>
<Show <Show when={billingInfo()?.paymentMethodLast4} fallback={<span data-slot="number">----</span>}>
when={billingInfo()?.paymentMethodLast4}
fallback={<span data-slot="number">----</span>}
>
<span data-slot="secret"></span> <span data-slot="secret"></span>
<span data-slot="number">{billingInfo()?.paymentMethodLast4}</span> <span data-slot="number">{billingInfo()?.paymentMethodLast4}</span>
</Show> </Show>
@@ -241,9 +225,7 @@ export function BillingSection() {
disabled={checkoutSubmission.pending || store.checkoutRedirecting} disabled={checkoutSubmission.pending || store.checkoutRedirecting}
onClick={onClickCheckout} onClick={onClickCheckout}
> >
{checkoutSubmission.pending || store.checkoutRedirecting {checkoutSubmission.pending || store.checkoutRedirecting ? "Loading..." : "Enable Billing"}
? "Loading..."
: "Enable Billing"}
</button> </button>
</Show> </Show>
</div> </div>

View File

@@ -104,13 +104,9 @@ export function MonthlyLimitSection() {
</button> </button>
</Show> </Show>
</div> </div>
<Show <Show when={billingInfo()?.monthlyLimit} fallback={<p data-slot="usage-status">No usage limit set.</p>}>
when={billingInfo()?.monthlyLimit}
fallback={<p data-slot="usage-status">No usage limit set.</p>}
>
<p data-slot="usage-status"> <p data-slot="usage-status">
Current usage for{" "} Current usage for {new Date().toLocaleDateString("en-US", { month: "long", timeZone: "UTC" })} is $
{new Date().toLocaleDateString("en-US", { month: "long", timeZone: "UTC" })} is $
{(() => { {(() => {
const dateLastUsed = billingInfo()?.timeMonthlyUsageUpdated const dateLastUsed = billingInfo()?.timeMonthlyUsageUpdated
if (!dateLastUsed) return "0" if (!dateLastUsed) return "0"

View File

@@ -89,10 +89,7 @@ export function PaymentSection() {
<td data-slot="payment-receipt"> <td data-slot="payment-receipt">
<button <button
onClick={async () => { onClick={async () => {
const receiptUrl = await downloadReceiptAction( const receiptUrl = await downloadReceiptAction(params.id, payment.paymentID!)
params.id,
payment.paymentID!,
)
if (receiptUrl) { if (receiptUrl) {
window.open(receiptUrl, "_blank") window.open(receiptUrl, "_blank")
} }

View File

@@ -69,11 +69,7 @@ export function ReloadSection() {
}) })
createEffect(() => { createEffect(() => {
if ( if (!setReloadSubmission.pending && setReloadSubmission.result && !(setReloadSubmission.result as any).error) {
!setReloadSubmission.pending &&
setReloadSubmission.result &&
!(setReloadSubmission.result as any).error
) {
setStore("show", false) setStore("show", false)
} }
}) })
@@ -108,8 +104,8 @@ export function ReloadSection() {
} }
> >
<p> <p>
Auto reload is <b>enabled</b>. We'll reload <b>${billingInfo()?.reloadAmount}</b>{" "} Auto reload is <b>enabled</b>. We'll reload <b>${billingInfo()?.reloadAmount}</b> (+$1.23 processing fee)
(+$1.23 processing fee) when balance reaches <b>${billingInfo()?.reloadTrigger}</b>. when balance reaches <b>${billingInfo()?.reloadTrigger}</b>.
</p> </p>
</Show> </Show>
<button data-color="primary" type="button" onClick={() => show()}> <button data-color="primary" type="button" onClick={() => show()}>
@@ -194,8 +190,8 @@ export function ReloadSection() {
minute: "2-digit", minute: "2-digit",
second: "2-digit", second: "2-digit",
})} })}
. Reason: {billingInfo()?.reloadError?.replace(/\.$/, "")}. Please update your payment . Reason: {billingInfo()?.reloadError?.replace(/\.$/, "")}. Please update your payment method and try
method and try again. again.
</p> </p>
<form action={reload} method="post" data-slot="create-form"> <form action={reload} method="post" data-slot="create-form">
<input type="hidden" name="workspaceID" value={params.id} /> <input type="hidden" name="workspaceID" value={params.id} />

View File

@@ -51,9 +51,7 @@ export default function () {
disabled={checkoutSubmission.pending || store.checkoutRedirecting} disabled={checkoutSubmission.pending || store.checkoutRedirecting}
onClick={onClickCheckout} onClick={onClickCheckout}
> >
{checkoutSubmission.pending || store.checkoutRedirecting {checkoutSubmission.pending || store.checkoutRedirecting ? "Loading..." : "Enable billing"}
? "Loading..."
: "Enable billing"}
</button> </button>
} }
> >

View File

@@ -146,20 +146,14 @@ export function KeySection() {
title="Copy API key" title="Copy API key"
> >
<span>{key.keyDisplay}</span> <span>{key.keyDisplay}</span>
<Show <Show when={copied()} fallback={<IconCopy style={{ width: "14px", height: "14px" }} />}>
when={copied()}
fallback={<IconCopy style={{ width: "14px", height: "14px" }} />}
>
<IconCheck style={{ width: "14px", height: "14px" }} /> <IconCheck style={{ width: "14px", height: "14px" }} />
</Show> </Show>
</button> </button>
</Show> </Show>
</td> </td>
<td data-slot="key-user-email">{key.email}</td> <td data-slot="key-user-email">{key.email}</td>
<td <td data-slot="key-last-used" title={key.timeUsed ? formatDateUTC(key.timeUsed) : undefined}>
data-slot="key-last-used"
title={key.timeUsed ? formatDateUTC(key.timeUsed) : undefined}
>
{key.timeUsed ? formatDateForTable(key.timeUsed) : "-"} {key.timeUsed ? formatDateForTable(key.timeUsed) : "-"}
</td> </td>
<td data-slot="key-actions"> <td data-slot="key-actions">

View File

@@ -85,12 +85,7 @@ const updateMember = action(async (form: FormData) => {
) )
}, "member.update") }, "member.update")
function MemberRow(props: { function MemberRow(props: { member: any; workspaceID: string; actorID: string; actorRole: string }) {
member: any
workspaceID: string
actorID: string
actorRole: string
}) {
const submission = useSubmission(updateMember) const submission = useSubmission(updateMember)
const isCurrentUser = () => props.actorID === props.member.id const isCurrentUser = () => props.actorID === props.member.id
const isAdmin = () => props.actorRole === "admin" const isAdmin = () => props.actorRole === "admin"

View File

@@ -5,15 +5,7 @@ import { withActor } from "~/context/auth.withActor"
import { ZenData } from "@opencode-ai/console-core/model.js" import { ZenData } from "@opencode-ai/console-core/model.js"
import styles from "./model-section.module.css" import styles from "./model-section.module.css"
import { querySessionInfo } from "../common" import { querySessionInfo } from "../common"
import { import { IconAlibaba, IconAnthropic, IconMoonshotAI, IconOpenAI, IconStealth, IconXai, IconZai } from "~/component/icon"
IconAlibaba,
IconAnthropic,
IconMoonshotAI,
IconOpenAI,
IconStealth,
IconXai,
IconZai,
} from "~/component/icon"
const getModelLab = (modelId: string) => { const getModelLab = (modelId: string) => {
if (modelId.startsWith("claude")) return "Anthropic" if (modelId.startsWith("claude")) return "Anthropic"
@@ -76,8 +68,7 @@ export function ModelSection() {
<div data-slot="section-title"> <div data-slot="section-title">
<h2>Models</h2> <h2>Models</h2>
<p> <p>
Manage which models workspace members can access.{" "} Manage which models workspace members can access. <a href="/docs/zen#pricing ">Learn more</a>.
<a href="/docs/zen#pricing ">Learn more</a>.
</p> </p>
</div> </div>
<div data-slot="models-list"> <div data-slot="models-list">

View File

@@ -43,24 +43,15 @@ export function NewUserSection() {
<div data-component="feature-grid"> <div data-component="feature-grid">
<div data-slot="feature"> <div data-slot="feature">
<h3>Tested & Verified Models</h3> <h3>Tested & Verified Models</h3>
<p> <p>We've benchmarked and tested models specifically for coding agents to ensure the best performance.</p>
We've benchmarked and tested models specifically for coding agents to ensure the best
performance.
</p>
</div> </div>
<div data-slot="feature"> <div data-slot="feature">
<h3>Highest Quality</h3> <h3>Highest Quality</h3>
<p> <p>Access models configured for optimal performance - no downgrades or routing to cheaper providers.</p>
Access models configured for optimal performance - no downgrades or routing to cheaper
providers.
</p>
</div> </div>
<div data-slot="feature"> <div data-slot="feature">
<h3>No Lock-in</h3> <h3>No Lock-in</h3>
<p> <p>Use Zen with any coding agent, and continue using other providers with opencode whenever you want.</p>
Use Zen with any coding agent, and continue using other providers with opencode
whenever you want.
</p>
</div> </div>
</div> </div>

View File

@@ -55,10 +55,7 @@ const listProviders = query(async (workspaceID: string) => {
function ProviderRow(props: { provider: Provider }) { function ProviderRow(props: { provider: Provider }) {
const params = useParams() const params = useParams()
const providers = createAsync(() => listProviders(params.id)) const providers = createAsync(() => listProviders(params.id))
const saveSubmission = useSubmission( const saveSubmission = useSubmission(saveProvider, ([fd]) => fd.get("provider")?.toString() === props.provider.key)
saveProvider,
([fd]) => fd.get("provider")?.toString() === props.provider.key,
)
const removeSubmission = useSubmission( const removeSubmission = useSubmission(
removeProvider, removeProvider,
([fd]) => fd.get("provider")?.toString() === props.provider.key, ([fd]) => fd.get("provider")?.toString() === props.provider.key,
@@ -94,16 +91,9 @@ function ProviderRow(props: { provider: Provider }) {
<td data-slot="provider-key"> <td data-slot="provider-key">
<Show <Show
when={store.editing} when={store.editing}
fallback={ fallback={<span>{providerData() ? maskCredentials(providerData()!.credentials) : "-"}</span>}
<span>{providerData() ? maskCredentials(providerData()!.credentials) : "-"}</span>
}
> >
<form <form id={`provider-form-${props.provider.key}`} action={saveProvider} method="post" data-slot="edit-form">
id={`provider-form-${props.provider.key}`}
action={saveProvider}
method="post"
data-slot="edit-form"
>
<div data-slot="input-wrapper"> <div data-slot="input-wrapper">
<input <input
ref={(r) => (input = r)} ref={(r) => (input = r)}

View File

@@ -67,10 +67,7 @@ export const querySessionInfo = query(async (workspaceID: string) => {
return withActor(() => { return withActor(() => {
return { return {
isAdmin: Actor.userRole() === "admin", isAdmin: Actor.userRole() === "admin",
isBeta: isBeta: Resource.App.stage === "production" ? workspaceID === "wrk_01K46JDFR0E75SG2Q8K172KF3Y" : true,
Resource.App.stage === "production"
? workspaceID === "wrk_01K46JDFR0E75SG2Q8K172KF3Y"
: true,
} }
}, workspaceID) }, workspaceID)
}, "session.get") }, "session.get")

View File

@@ -29,10 +29,7 @@ export default function Home() {
createAsync(() => checkLoggedIn()) createAsync(() => checkLoggedIn())
return ( return (
<main data-page="zen"> <main data-page="zen">
<HttpHeader <HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />
name="Cache-Control"
value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400"
/>
<Title>OpenCode Zen | A curated set of reliable optimized models for coding agents</Title> <Title>OpenCode Zen | A curated set of reliable optimized models for coding agents</Title>
<Link rel="canonical" href={`${config.baseUrl}/zen`} /> <Link rel="canonical" href={`${config.baseUrl}/zen`} />
<Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" /> <Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" />
@@ -49,19 +46,13 @@ export default function Home() {
<img data-slot="zen logo dark" src={zenLogoDark} alt="zen logo dark" /> <img data-slot="zen logo dark" src={zenLogoDark} alt="zen logo dark" />
<h1>Reliable optimized models for coding agents</h1> <h1>Reliable optimized models for coding agents</h1>
<p> <p>
Zen gives you access to a curated set of AI models that OpenCode has tested and Zen gives you access to a curated set of AI models that OpenCode has tested and benchmarked specifically
benchmarked specifically for coding agents. No need to worry about inconsistent for coding agents. No need to worry about inconsistent performance and quality, use validated models
performance and quality, use validated models that work. that work.
</p> </p>
<div data-slot="model-logos"> <div data-slot="model-logos">
<div> <div>
<svg <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<mask <mask
id="mask0_79_128586" id="mask0_79_128586"
style="mask-type:luminance" style="mask-type:luminance"
@@ -82,17 +73,8 @@ export default function Home() {
</svg> </svg>
</div> </div>
<div> <div>
<svg <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
width="24" <path d="M13.7891 3.93164L20.2223 20.0677H23.7502L17.317 3.93164H13.7891Z" fill="currentColor" />
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.7891 3.93164L20.2223 20.0677H23.7502L17.317 3.93164H13.7891Z"
fill="currentColor"
/>
<path <path
d="M6.32538 13.6824L8.52662 8.01177L10.7279 13.6824H6.32538ZM6.68225 3.93164L0.25 20.0677H3.84652L5.16202 16.6791H11.8914L13.2067 20.0677H16.8033L10.371 3.93164H6.68225Z" d="M6.32538 13.6824L8.52662 8.01177L10.7279 13.6824H6.32538ZM6.68225 3.93164L0.25 20.0677H3.84652L5.16202 16.6791H11.8914L13.2067 20.0677H16.8033L10.371 3.93164H6.68225Z"
fill="currentColor" fill="currentColor"
@@ -100,13 +82,7 @@ export default function Home() {
</svg> </svg>
</div> </div>
<div> <div>
<svg <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M9.16861 16.0529L17.2018 9.85156C17.5957 9.54755 18.1586 9.66612 18.3463 10.1384C19.3339 12.6288 18.8926 15.6217 16.9276 17.6766C14.9626 19.7314 12.2285 20.1821 9.72948 19.1557L6.9995 20.4775C10.9151 23.2763 15.6699 22.5841 18.6411 19.4749C20.9979 17.0103 21.7278 13.6508 21.0453 10.6214L21.0515 10.6278C20.0617 6.17736 21.2948 4.39847 23.8207 0.760904C23.8804 0.674655 23.9402 0.588405 24 0.5L20.6762 3.97585V3.96506L9.16658 16.0551" d="M9.16861 16.0529L17.2018 9.85156C17.5957 9.54755 18.1586 9.66612 18.3463 10.1384C19.3339 12.6288 18.8926 15.6217 16.9276 17.6766C14.9626 19.7314 12.2285 20.1821 9.72948 19.1557L6.9995 20.4775C10.9151 23.2763 15.6699 22.5841 18.6411 19.4749C20.9979 17.0103 21.7278 13.6508 21.0453 10.6214L21.0515 10.6278C20.0617 6.17736 21.2948 4.39847 23.8207 0.760904C23.8804 0.674655 23.9402 0.588405 24 0.5L20.6762 3.97585V3.96506L9.16658 16.0551"
fill="currentColor" fill="currentColor"
@@ -118,13 +94,7 @@ export default function Home() {
</svg> </svg>
</div> </div>
<div> <div>
<svg <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
fill-rule="evenodd" fill-rule="evenodd"
clip-rule="evenodd" clip-rule="evenodd"
@@ -134,13 +104,7 @@ export default function Home() {
</svg> </svg>
</div> </div>
<div> <div>
<svg <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M12.6241 11.346L20.3848 3.44816C20.5309 3.29931 20.4487 3 20.2601 3H16.0842C16.0388 3 15.9949 3.01897 15.9594 3.05541L7.59764 11.5629C7.46721 11.6944 7.27446 11.5771 7.27446 11.3666V3.25183C7.27446 3.11242 7.18515 3 7.07594 3H4.19843C4.08932 3 4 3.11242 4 3.25183V20.7482C4 20.8876 4.08932 21 4.19843 21H7.07594C7.18515 21 7.27446 20.8876 7.27446 20.7482V17.1834C7.27446 17.1073 7.30136 17.0344 7.34815 16.987L9.94075 14.3486C10.0031 14.2853 10.0895 14.2757 10.159 14.3232L17.0934 19.5573C18.2289 20.3412 19.4975 20.8226 20.786 20.9652C20.9008 20.9778 21 20.8606 21 20.7133V17.3559C21 17.2276 20.9249 17.1232 20.8243 17.1073C20.0659 16.9853 19.326 16.6845 18.6569 16.222L12.6538 11.764C12.5291 11.6785 12.5135 11.4584 12.6241 11.346Z" d="M12.6241 11.346L20.3848 3.44816C20.5309 3.29931 20.4487 3 20.2601 3H16.0842C16.0388 3 15.9949 3.01897 15.9594 3.05541L7.59764 11.5629C7.46721 11.6944 7.27446 11.5771 7.27446 11.3666V3.25183C7.27446 3.11242 7.18515 3 7.07594 3H4.19843C4.08932 3 4 3.11242 4 3.25183V20.7482C4 20.8876 4.08932 21 4.19843 21H7.07594C7.18515 21 7.27446 20.8876 7.27446 20.7482V17.1834C7.27446 17.1073 7.30136 17.0344 7.34815 16.987L9.94075 14.3486C10.0031 14.2853 10.0895 14.2757 10.159 14.3232L17.0934 19.5573C18.2289 20.3412 19.4975 20.8226 20.786 20.9652C20.9008 20.9778 21 20.8606 21 20.7133V17.3559C21 17.2276 20.9249 17.1232 20.8243 17.1073C20.0659 16.9853 19.326 16.6845 18.6569 16.222L12.6538 11.764C12.5291 11.6785 12.5135 11.4584 12.6241 11.346Z"
fill="currentColor" fill="currentColor"
@@ -150,13 +114,7 @@ export default function Home() {
</div> </div>
<a href="/auth"> <a href="/auth">
<span>Get started with Zen </span> <span>Get started with Zen </span>
<svg <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M6.5 12L17 12M13 16.5L17.5 12L13 7.5" d="M6.5 12L17 12M13 16.5L17.5 12L13 7.5"
stroke="currentColor" stroke="currentColor"
@@ -168,23 +126,14 @@ export default function Home() {
</div> </div>
<div data-slot="pricing-copy"> <div data-slot="pricing-copy">
<p> <p>
<strong>Add $20 Pay as you go balance</strong>{" "} <strong>Add $20 Pay as you go balance</strong> <span>(+$1.23 card processing fee)</span>
<span>(+$1.23 card processing fee)</span>
</p> </p>
<p>Use with any agent. Set monthly spend limits. Cancel any time.</p> <p>Use with any agent. Set monthly spend limits. Cancel any time.</p>
</div> </div>
</section> </section>
<section data-component="comparison"> <section data-component="comparison">
<video <video src={compareVideo} autoplay playsinline loop muted preload="auto" poster={compareVideoPoster}>
src={compareVideo}
autoplay
playsinline
loop
muted
preload="auto"
poster={compareVideoPoster}
>
Your browser does not support the video tag. Your browser does not support the video tag.
</video> </video>
</section> </section>
@@ -193,8 +142,8 @@ export default function Home() {
<div data-slot="section-title"> <div data-slot="section-title">
<h3>What problem is Zen solving?</h3> <h3>What problem is Zen solving?</h3>
<p> <p>
There are so many models available, but only a few work well with coding agents. There are so many models available, but only a few work well with coding agents. Most providers
Most providers configure them differently with varying results. configure them differently with varying results.
</p> </p>
</div> </div>
<p>We're fixing this for everyone, not just OpenCode users.</p> <p>We're fixing this for everyone, not just OpenCode users.</p>
@@ -229,15 +178,14 @@ export default function Home() {
<li> <li>
<span>[2]</span> <span>[2]</span>
<div> <div>
<strong>Use Zen with transparent pricing</strong> -{" "} <strong>Use Zen with transparent pricing</strong> - <a href="/docs/zen/#pricing">pay per request</a>{" "}
<a href="/docs/zen/#pricing">pay per request</a> with zero markups with zero markups
</div> </div>
</li> </li>
<li> <li>
<span>[3]</span> <span>[3]</span>
<div> <div>
<strong>Auto-top up</strong> - when your balance reaches $5 well automatically <strong>Auto-top up</strong> - when your balance reaches $5 well automatically add $20
add $20
</div> </div>
</li> </li>
</ul> </ul>
@@ -249,9 +197,8 @@ export default function Home() {
<div> <div>
<span>[*]</span> <span>[*]</span>
<p> <p>
All Zen models are hosted in the US. Providers follow a zero-retention policy and All Zen models are hosted in the US. Providers follow a zero-retention policy and do not use your data
do not use your data for model training, with the{" "} for model training, with the <a href="/docs/zen/#privacy">following exceptions</a>.
<a href="/docs/zen/#privacy">following exceptions</a>.
</p> </p>
</div> </div>
</div> </div>
@@ -306,8 +253,7 @@ export default function Home() {
<span>ex-Head of Design, Laravel</span> <span>ex-Head of Design, Laravel</span>
</div> </div>
<div data-slot="quote"> <div data-slot="quote">
With <span>@OpenCode</span> Zen I know all the models are tested and perfect for With <span>@OpenCode</span> Zen I know all the models are tested and perfect for coding agents.
coding agents.
</div> </div>
</div> </div>
</a> </a>
@@ -331,44 +277,38 @@ export default function Home() {
<ul> <ul>
<li> <li>
<Faq question="What is OpenCode Zen?"> <Faq question="What is OpenCode Zen?">
Zen is a curated set of AI models tested and benchmarked for coding agents created Zen is a curated set of AI models tested and benchmarked for coding agents created by the team behind
by the team behind OpenCode. OpenCode.
</Faq> </Faq>
</li> </li>
<li> <li>
<Faq question="What makes Zen more accurate?"> <Faq question="What makes Zen more accurate?">
Zen only provides models that have been specifically tested and benchmarked for Zen only provides models that have been specifically tested and benchmarked for coding agents. You
coding agents. You wouldnt use a butter knife to cut steak, dont use poor models wouldnt use a butter knife to cut steak, dont use poor models for coding.
for coding.
</Faq> </Faq>
</li> </li>
<li> <li>
<Faq question="Is Zen cheaper?"> <Faq question="Is Zen cheaper?">
Zen is not for profit. Zen passes through the costs from the model providers to Zen is not for profit. Zen passes through the costs from the model providers to you. The higher Zens
you. The higher Zens usage the more OpenCode can negotiate better rates and pass usage the more OpenCode can negotiate better rates and pass those to you.
those to you.
</Faq> </Faq>
</li> </li>
<li> <li>
<Faq question="How much does Zen cost?"> <Faq question="How much does Zen cost?">
Zen <a href="/docs/zen/#pricing">charges per request</a> with zero markups, so you Zen <a href="/docs/zen/#pricing">charges per request</a> with zero markups, so you pay exactly what
pay exactly what the model provider charges. Your total cost depends on usage, and the model provider charges. Your total cost depends on usage, and you can set monthly spend limits in
you can set monthly spend limits in your <a href="/auth">account</a>. To cover your <a href="/auth">account</a>. To cover costs, OpenCode adds only a small payment processing fee of
costs, OpenCode adds only a small payment processing fee of $1.23 per $20 balance $1.23 per $20 balance top-up.
top-up.
</Faq> </Faq>
</li> </li>
<li> <li>
<Faq question="What about data and privacy?"> <Faq question="What about data and privacy?">
All Zen models are hosted in the US. Providers follow a zero-retention policy and All Zen models are hosted in the US. Providers follow a zero-retention policy and do not use your data
do not use your data for model training, with the{" "} for model training, with the <a href="/docs/zen/#privacy">following exceptions</a>.
<a href="/docs/zen/#privacy">following exceptions</a>.
</Faq> </Faq>
</li> </li>
<li> <li>
<Faq question="Can I set spend limits?"> <Faq question="Can I set spend limits?">Yes, you can set monthly spending limits in your account.</Faq>
Yes, you can set monthly spending limits in your account.
</Faq>
</li> </li>
<li> <li>
<Faq question="Can I cancel?"> <Faq question="Can I cancel?">
@@ -377,8 +317,8 @@ export default function Home() {
</li> </li>
<li> <li>
<Faq question="Can I use Zen with other coding agents?"> <Faq question="Can I use Zen with other coding agents?">
While Zen works great with OpenCode, you can use Zen with any agent. Follow the While Zen works great with OpenCode, you can use Zen with any agent. Follow the setup instructions in
setup instructions in your preferred coding agent. your preferred coding agent.
</Faq> </Faq>
</li> </li>
</ul> </ul>

View File

@@ -13,11 +13,7 @@ import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js" import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js"
import { logger } from "./logger" import { logger } from "./logger"
import { AuthError, CreditsError, MonthlyLimitError, UserLimitError, ModelError } from "./error" import { AuthError, CreditsError, MonthlyLimitError, UserLimitError, ModelError } from "./error"
import { import { createBodyConverter, createStreamPartConverter, createResponseConverter } from "./provider/provider"
createBodyConverter,
createStreamPartConverter,
createResponseConverter,
} from "./provider/provider"
import { anthropicHelper } from "./provider/anthropic" import { anthropicHelper } from "./provider/anthropic"
import { openaiHelper } from "./provider/openai" import { openaiHelper } from "./provider/openai"
import { oaCompatHelper } from "./provider/openai-compatible" import { oaCompatHelper } from "./provider/openai-compatible"
@@ -46,11 +42,7 @@ export async function handler(
}) })
const zenData = ZenData.list() const zenData = ZenData.list()
const modelInfo = validateModel(zenData, body.model) const modelInfo = validateModel(zenData, body.model)
const providerInfo = selectProvider( const providerInfo = selectProvider(zenData, modelInfo, input.request.headers.get("x-real-ip") ?? "")
zenData,
modelInfo,
input.request.headers.get("x-real-ip") ?? "",
)
const authInfo = await authenticate(modelInfo, providerInfo) const authInfo = await authenticate(modelInfo, providerInfo)
validateBilling(modelInfo, authInfo) validateBilling(modelInfo, authInfo)
validateModelSettings(authInfo) validateModelSettings(authInfo)
@@ -229,11 +221,7 @@ export async function handler(
return { id: modelId, ...modelData } return { id: modelId, ...modelData }
} }
function selectProvider( function selectProvider(zenData: ZenData, model: Awaited<ReturnType<typeof validateModel>>, ip: string) {
zenData: ZenData,
model: Awaited<ReturnType<typeof validateModel>>,
ip: string,
) {
const providers = model.providers const providers = model.providers
.filter((provider) => !provider.disabled) .filter((provider) => !provider.disabled)
.flatMap((provider) => Array<typeof provider>(provider.weight ?? 1).fill(provider)) .flatMap((provider) => Array<typeof provider>(provider.weight ?? 1).fill(provider))
@@ -252,11 +240,7 @@ export async function handler(
return { return {
...provider, ...provider,
...zenData.providers[provider.id], ...zenData.providers[provider.id],
...(format === "anthropic" ...(format === "anthropic" ? anthropicHelper : format === "openai" ? openaiHelper : oaCompatHelper),
? anthropicHelper
: format === "openai"
? openaiHelper
: oaCompatHelper),
} }
} }
@@ -297,20 +281,11 @@ export async function handler(
.from(KeyTable) .from(KeyTable)
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID)) .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID))
.innerJoin(BillingTable, eq(BillingTable.workspaceID, KeyTable.workspaceID)) .innerJoin(BillingTable, eq(BillingTable.workspaceID, KeyTable.workspaceID))
.innerJoin( .innerJoin(UserTable, and(eq(UserTable.workspaceID, KeyTable.workspaceID), eq(UserTable.id, KeyTable.userID)))
UserTable, .leftJoin(ModelTable, and(eq(ModelTable.workspaceID, KeyTable.workspaceID), eq(ModelTable.model, model.id)))
and(eq(UserTable.workspaceID, KeyTable.workspaceID), eq(UserTable.id, KeyTable.userID)),
)
.leftJoin(
ModelTable,
and(eq(ModelTable.workspaceID, KeyTable.workspaceID), eq(ModelTable.model, model.id)),
)
.leftJoin( .leftJoin(
ProviderTable, ProviderTable,
and( and(eq(ProviderTable.workspaceID, KeyTable.workspaceID), eq(ProviderTable.provider, providerInfo.id)),
eq(ProviderTable.workspaceID, KeyTable.workspaceID),
eq(ProviderTable.provider, providerInfo.id),
),
) )
.where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted))) .where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
.then((rows) => rows[0]), .then((rows) => rows[0]),
@@ -401,19 +376,12 @@ export async function handler(
providerInfo: Awaited<ReturnType<typeof selectProvider>>, providerInfo: Awaited<ReturnType<typeof selectProvider>>,
usage: any, usage: any,
) { ) {
const { const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
inputTokens, providerInfo.normalizeUsage(usage)
outputTokens,
reasoningTokens,
cacheReadTokens,
cacheWrite5mTokens,
cacheWrite1hTokens,
} = providerInfo.normalizeUsage(usage)
const modelCost = const modelCost =
modelInfo.cost200K && modelInfo.cost200K &&
inputTokens + (cacheReadTokens ?? 0) + (cacheWrite5mTokens ?? 0) + (cacheWrite1hTokens ?? 0) > inputTokens + (cacheReadTokens ?? 0) + (cacheWrite5mTokens ?? 0) + (cacheWrite1hTokens ?? 0) > 200_000
200_000
? modelInfo.cost200K ? modelInfo.cost200K
: modelInfo.cost : modelInfo.cost
@@ -464,8 +432,7 @@ export async function handler(
if (!authInfo) return if (!authInfo) return
const cost = const cost = authInfo.isFree || authInfo.provider?.credentials ? 0 : centsToMicroCents(totalCostInCent)
authInfo.isFree || authInfo.provider?.credentials ? 0 : centsToMicroCents(totalCostInCent)
await Database.transaction(async (tx) => { await Database.transaction(async (tx) => {
await tx.insert(UsageTable).values({ await tx.insert(UsageTable).values({
workspaceID: authInfo.workspaceID, workspaceID: authInfo.workspaceID,
@@ -505,9 +472,7 @@ export async function handler(
`, `,
timeMonthlyUsageUpdated: sql`now()`, timeMonthlyUsageUpdated: sql`now()`,
}) })
.where( .where(and(eq(UserTable.workspaceID, authInfo.workspaceID), eq(UserTable.id, authInfo.user.id)))
and(eq(UserTable.workspaceID, authInfo.workspaceID), eq(UserTable.id, authInfo.user.id)),
)
}) })
await Database.use((tx) => await Database.use((tx) =>
@@ -537,10 +502,7 @@ export async function handler(
BillingTable.balance, BillingTable.balance,
centsToMicroCents((authInfo.billing.reloadTrigger ?? Billing.RELOAD_TRIGGER) * 100), centsToMicroCents((authInfo.billing.reloadTrigger ?? Billing.RELOAD_TRIGGER) * 100),
), ),
or( or(isNull(BillingTable.timeReloadLockedTill), lt(BillingTable.timeReloadLockedTill, sql`now()`)),
isNull(BillingTable.timeReloadLockedTill),
lt(BillingTable.timeReloadLockedTill, sql`now()`),
),
), ),
), ),
) )

View File

@@ -123,15 +123,12 @@ export function fromAnthropicRequest(body: any): CommonRequest {
if ((p as any).type === "tool_result") { if ((p as any).type === "tool_result") {
const id = (p as any).tool_use_id const id = (p as any).tool_use_id
const content = const content =
typeof (p as any).content === "string" typeof (p as any).content === "string" ? (p as any).content : JSON.stringify((p as any).content)
? (p as any).content
: JSON.stringify((p as any).content)
msgs.push({ role: "tool", tool_call_id: id, content }) msgs.push({ role: "tool", tool_call_id: id, content })
} }
} }
if (partsOut.length > 0) { if (partsOut.length > 0) {
if (partsOut.length === 1 && partsOut[0].type === "text") if (partsOut.length === 1 && partsOut[0].type === "text") msgs.push({ role: "user", content: partsOut[0].text })
msgs.push({ role: "user", content: partsOut[0].text })
else msgs.push({ role: "user", content: partsOut }) else msgs.push({ role: "user", content: partsOut })
} }
continue continue
@@ -143,8 +140,7 @@ export function fromAnthropicRequest(body: any): CommonRequest {
const tcs: any[] = [] const tcs: any[] = []
for (const p of partsIn) { for (const p of partsIn) {
if (!p || !(p as any).type) continue if (!p || !(p as any).type) continue
if ((p as any).type === "text" && typeof (p as any).text === "string") if ((p as any).type === "text" && typeof (p as any).text === "string") texts.push((p as any).text)
texts.push((p as any).text)
if ((p as any).type === "tool_use") { if ((p as any).type === "tool_use") {
const name = (p as any).name const name = (p as any).name
const id = (p as any).id const id = (p as any).id
@@ -214,9 +210,7 @@ export function fromAnthropicRequest(body: any): CommonRequest {
export function toAnthropicRequest(body: CommonRequest) { export function toAnthropicRequest(body: CommonRequest) {
if (!body || typeof body !== "object") return body if (!body || typeof body !== "object") return body
const sysIn = Array.isArray(body.messages) const sysIn = Array.isArray(body.messages) ? body.messages.filter((m: any) => m && m.role === "system") : []
? body.messages.filter((m: any) => m && m.role === "system")
: []
let ccCount = 0 let ccCount = 0
const cc = () => { const cc = () => {
ccCount++ ccCount++
@@ -367,9 +361,7 @@ export function fromAnthropicResponse(resp: any): CommonResponse {
const idIn = (resp as any).id const idIn = (resp as any).id
const id = const id =
typeof idIn === "string" typeof idIn === "string" ? idIn.replace(/^msg_/, "chatcmpl_") : `chatcmpl_${Math.random().toString(36).slice(2)}`
? idIn.replace(/^msg_/, "chatcmpl_")
: `chatcmpl_${Math.random().toString(36).slice(2)}`
const model = (resp as any).model const model = (resp as any).model
const blocks: any[] = Array.isArray((resp as any).content) ? (resp as any).content : [] const blocks: any[] = Array.isArray((resp as any).content) ? (resp as any).content : []
@@ -412,9 +404,7 @@ export function fromAnthropicResponse(resp: any): CommonResponse {
const ct = typeof (u as any).output_tokens === "number" ? (u as any).output_tokens : undefined const ct = typeof (u as any).output_tokens === "number" ? (u as any).output_tokens : undefined
const total = pt != null && ct != null ? pt + ct : undefined const total = pt != null && ct != null ? pt + ct : undefined
const cached = const cached =
typeof (u as any).cache_read_input_tokens === "number" typeof (u as any).cache_read_input_tokens === "number" ? (u as any).cache_read_input_tokens : undefined
? (u as any).cache_read_input_tokens
: undefined
const details = cached != null ? { cached_tokens: cached } : undefined const details = cached != null ? { cached_tokens: cached } : undefined
return { return {
prompt_tokens: pt, prompt_tokens: pt,
@@ -591,9 +581,7 @@ export function fromAnthropicChunk(chunk: string): CommonChunk | string {
prompt_tokens: u.input_tokens, prompt_tokens: u.input_tokens,
completion_tokens: u.output_tokens, completion_tokens: u.output_tokens,
total_tokens: (u.input_tokens || 0) + (u.output_tokens || 0), total_tokens: (u.input_tokens || 0) + (u.output_tokens || 0),
...(u.cache_read_input_tokens ...(u.cache_read_input_tokens ? { prompt_tokens_details: { cached_tokens: u.cache_read_input_tokens } } : {}),
? { prompt_tokens_details: { cached_tokens: u.cache_read_input_tokens } }
: {}),
} }
} }

View File

@@ -57,8 +57,7 @@ export const oaCompatHelper = {
const inputTokens = usage.prompt_tokens ?? 0 const inputTokens = usage.prompt_tokens ?? 0
const outputTokens = usage.completion_tokens ?? 0 const outputTokens = usage.completion_tokens ?? 0
const reasoningTokens = usage.completion_tokens_details?.reasoning_tokens ?? undefined const reasoningTokens = usage.completion_tokens_details?.reasoning_tokens ?? undefined
const cacheReadTokens = const cacheReadTokens = usage.cached_tokens ?? usage.prompt_tokens_details?.cached_tokens ?? undefined
usage.cached_tokens ?? usage.prompt_tokens_details?.cached_tokens ?? undefined
return { return {
inputTokens: inputTokens - (cacheReadTokens ?? 0), inputTokens: inputTokens - (cacheReadTokens ?? 0),
outputTokens, outputTokens,
@@ -80,8 +79,7 @@ export function fromOaCompatibleRequest(body: any): CommonRequest {
if (!m || !m.role) continue if (!m || !m.role) continue
if (m.role === "system") { if (m.role === "system") {
if (typeof m.content === "string" && m.content.length > 0) if (typeof m.content === "string" && m.content.length > 0) msgsOut.push({ role: "system", content: m.content })
msgsOut.push({ role: "system", content: m.content })
continue continue
} }
@@ -92,12 +90,10 @@ export function fromOaCompatibleRequest(body: any): CommonRequest {
const parts: any[] = [] const parts: any[] = []
for (const p of m.content) { for (const p of m.content) {
if (!p || !p.type) continue if (!p || !p.type) continue
if (p.type === "text" && typeof p.text === "string") if (p.type === "text" && typeof p.text === "string") parts.push({ type: "text", text: p.text })
parts.push({ type: "text", text: p.text })
if (p.type === "image_url") parts.push({ type: "image_url", image_url: p.image_url }) if (p.type === "image_url") parts.push({ type: "image_url", image_url: p.image_url })
} }
if (parts.length === 1 && parts[0].type === "text") if (parts.length === 1 && parts[0].type === "text") msgsOut.push({ role: "user", content: parts[0].text })
msgsOut.push({ role: "user", content: parts[0].text })
else if (parts.length > 0) msgsOut.push({ role: "user", content: parts }) else if (parts.length > 0) msgsOut.push({ role: "user", content: parts })
} }
continue continue
@@ -141,8 +137,7 @@ export function toOaCompatibleRequest(body: CommonRequest) {
if (p.type === "image_url" && p.image_url) return { type: "image_url", image_url: p.image_url } if (p.type === "image_url" && p.image_url) return { type: "image_url", image_url: p.image_url }
const s = (p as any).source const s = (p as any).source
if (!s || typeof s !== "object") return undefined if (!s || typeof s !== "object") return undefined
if (s.type === "url" && typeof s.url === "string") if (s.type === "url" && typeof s.url === "string") return { type: "image_url", image_url: { url: s.url } }
return { type: "image_url", image_url: { url: s.url } }
if (s.type === "base64" && typeof s.media_type === "string" && typeof s.data === "string") if (s.type === "base64" && typeof s.media_type === "string" && typeof s.data === "string")
return { type: "image_url", image_url: { url: `data:${s.media_type};base64,${s.data}` } } return { type: "image_url", image_url: { url: `data:${s.media_type};base64,${s.data}` } }
return undefined return undefined
@@ -152,8 +147,7 @@ export function toOaCompatibleRequest(body: CommonRequest) {
if (!m || !m.role) continue if (!m || !m.role) continue
if (m.role === "system") { if (m.role === "system") {
if (typeof m.content === "string" && m.content.length > 0) if (typeof m.content === "string" && m.content.length > 0) msgsOut.push({ role: "system", content: m.content })
msgsOut.push({ role: "system", content: m.content })
continue continue
} }
@@ -166,13 +160,11 @@ export function toOaCompatibleRequest(body: CommonRequest) {
const parts: any[] = [] const parts: any[] = []
for (const p of m.content) { for (const p of m.content) {
if (!p || !p.type) continue if (!p || !p.type) continue
if (p.type === "text" && typeof p.text === "string") if (p.type === "text" && typeof p.text === "string") parts.push({ type: "text", text: p.text })
parts.push({ type: "text", text: p.text })
const ip = toImg(p) const ip = toImg(p)
if (ip) parts.push(ip) if (ip) parts.push(ip)
} }
if (parts.length === 1 && parts[0].type === "text") if (parts.length === 1 && parts[0].type === "text") msgsOut.push({ role: "user", content: parts[0].text })
msgsOut.push({ role: "user", content: parts[0].text })
else if (parts.length > 0) msgsOut.push({ role: "user", content: parts }) else if (parts.length > 0) msgsOut.push({ role: "user", content: parts })
} }
continue continue
@@ -325,9 +317,7 @@ export function toOaCompatibleResponse(resp: CommonResponse) {
const idIn = (resp as any).id const idIn = (resp as any).id
const id = const id =
typeof idIn === "string" typeof idIn === "string" ? idIn.replace(/^msg_/, "chatcmpl_") : `chatcmpl_${Math.random().toString(36).slice(2)}`
? idIn.replace(/^msg_/, "chatcmpl_")
: `chatcmpl_${Math.random().toString(36).slice(2)}`
const model = (resp as any).model const model = (resp as any).model
const blocks: any[] = Array.isArray((resp as any).content) ? (resp as any).content : [] const blocks: any[] = Array.isArray((resp as any).content) ? (resp as any).content : []
@@ -369,8 +359,7 @@ export function toOaCompatibleResponse(resp: CommonResponse) {
const pt = typeof u.input_tokens === "number" ? u.input_tokens : undefined const pt = typeof u.input_tokens === "number" ? u.input_tokens : undefined
const ct = typeof u.output_tokens === "number" ? u.output_tokens : undefined const ct = typeof u.output_tokens === "number" ? u.output_tokens : undefined
const total = pt != null && ct != null ? pt + ct : undefined const total = pt != null && ct != null ? pt + ct : undefined
const cached = const cached = typeof u.cache_read_input_tokens === "number" ? u.cache_read_input_tokens : undefined
typeof u.cache_read_input_tokens === "number" ? u.cache_read_input_tokens : undefined
const details = cached != null ? { cached_tokens: cached } : undefined const details = cached != null ? { cached_tokens: cached } : undefined
return { return {
prompt_tokens: pt, prompt_tokens: pt,

View File

@@ -86,11 +86,7 @@ export function fromOpenaiRequest(body: any): CommonRequest {
const msgs: any[] = [] const msgs: any[] = []
const inMsgs = Array.isArray(body.input) const inMsgs = Array.isArray(body.input) ? body.input : Array.isArray(body.messages) ? body.messages : []
? body.input
: Array.isArray(body.messages)
? body.messages
: []
for (const m of inMsgs) { for (const m of inMsgs) {
if (!m) continue if (!m) continue
@@ -103,9 +99,7 @@ export function fromOpenaiRequest(body: any): CommonRequest {
const args = typeof a === "string" ? a : JSON.stringify(a ?? {}) const args = typeof a === "string" ? a : JSON.stringify(a ?? {})
msgs.push({ msgs.push({
role: "assistant", role: "assistant",
tool_calls: [ tool_calls: [{ id: (m as any).id, type: "function", function: { name, arguments: args } }],
{ id: (m as any).id, type: "function", function: { name, arguments: args } },
],
}) })
} }
if ((m as any).type === "function_call_output") { if ((m as any).type === "function_call_output") {
@@ -122,8 +116,7 @@ export function fromOpenaiRequest(body: any): CommonRequest {
if (typeof c === "string" && c.length > 0) msgs.push({ role: "system", content: c }) if (typeof c === "string" && c.length > 0) msgs.push({ role: "system", content: c })
if (Array.isArray(c)) { if (Array.isArray(c)) {
const t = c.find((p: any) => p && typeof p.text === "string") const t = c.find((p: any) => p && typeof p.text === "string")
if (t && typeof t.text === "string" && t.text.length > 0) if (t && typeof t.text === "string" && t.text.length > 0) msgs.push({ role: "system", content: t.text })
msgs.push({ role: "system", content: t.text })
} }
continue continue
} }
@@ -136,24 +129,18 @@ export function fromOpenaiRequest(body: any): CommonRequest {
const parts: any[] = [] const parts: any[] = []
for (const p of c) { for (const p of c) {
if (!p || !(p as any).type) continue if (!p || !(p as any).type) continue
if ( if (((p as any).type === "text" || (p as any).type === "input_text") && typeof (p as any).text === "string")
((p as any).type === "text" || (p as any).type === "input_text") &&
typeof (p as any).text === "string"
)
parts.push({ type: "text", text: (p as any).text }) parts.push({ type: "text", text: (p as any).text })
const ip = toImg(p) const ip = toImg(p)
if (ip) parts.push(ip) if (ip) parts.push(ip)
if ((p as any).type === "tool_result") { if ((p as any).type === "tool_result") {
const id = (p as any).tool_call_id const id = (p as any).tool_call_id
const content = const content =
typeof (p as any).content === "string" typeof (p as any).content === "string" ? (p as any).content : JSON.stringify((p as any).content)
? (p as any).content
: JSON.stringify((p as any).content)
msgs.push({ role: "tool", tool_call_id: id, content }) msgs.push({ role: "tool", tool_call_id: id, content })
} }
} }
if (parts.length === 1 && parts[0].type === "text") if (parts.length === 1 && parts[0].type === "text") msgs.push({ role: "user", content: parts[0].text })
msgs.push({ role: "user", content: parts[0].text })
else if (parts.length > 0) msgs.push({ role: "user", content: parts }) else if (parts.length > 0) msgs.push({ role: "user", content: parts })
} }
continue continue
@@ -280,10 +267,7 @@ export function toOpenaiRequest(body: CommonRequest) {
} }
if ((m as any).role === "tool") { if ((m as any).role === "tool") {
const out = const out = typeof (m as any).content === "string" ? (m as any).content : JSON.stringify((m as any).content)
typeof (m as any).content === "string"
? (m as any).content
: JSON.stringify((m as any).content)
input.push({ type: "function_call_output", call_id: (m as any).tool_call_id, output: out }) input.push({ type: "function_call_output", call_id: (m as any).tool_call_id, output: out })
continue continue
} }
@@ -351,9 +335,7 @@ export function fromOpenaiResponse(resp: any): CommonResponse {
const idIn = (r as any).id const idIn = (r as any).id
const id = const id =
typeof idIn === "string" typeof idIn === "string" ? idIn.replace(/^resp_/, "chatcmpl_") : `chatcmpl_${Math.random().toString(36).slice(2)}`
? idIn.replace(/^resp_/, "chatcmpl_")
: `chatcmpl_${Math.random().toString(36).slice(2)}`
const model = (r as any).model ?? (resp as any).model const model = (r as any).model ?? (resp as any).model
const out = Array.isArray((r as any).output) ? (r as any).output : [] const out = Array.isArray((r as any).output) ? (r as any).output : []
@@ -480,9 +462,7 @@ export function toOpenaiResponse(resp: CommonResponse) {
})() })()
return { return {
id: id: (resp as any).id?.replace(/^chatcmpl_/, "resp_") ?? `resp_${Math.random().toString(36).slice(2)}`,
(resp as any).id?.replace(/^chatcmpl_/, "resp_") ??
`resp_${Math.random().toString(36).slice(2)}`,
object: "response", object: "response",
model: (resp as any).model, model: (resp as any).model,
output: outputItems, output: outputItems,

View File

@@ -50,10 +50,7 @@ export async function GET(input: APIEvent) {
}) })
.from(KeyTable) .from(KeyTable)
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID)) .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID))
.leftJoin( .leftJoin(ModelTable, and(eq(ModelTable.workspaceID, KeyTable.workspaceID), isNull(ModelTable.timeDeleted)))
ModelTable,
and(eq(ModelTable.workspaceID, KeyTable.workspaceID), isNull(ModelTable.timeDeleted)),
)
.where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted))) .where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
.then((rows) => rows.map((row) => row.model)), .then((rows) => rows.map((row) => row.model)),
) )

View File

@@ -15,7 +15,6 @@ body {
--font-size-9xl: 8rem; --font-size-9xl: 8rem;
--font-mono: --font-mono:
"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
"Courier New", monospace;
--font-sans: var(--font-mono); --font-sans: var(--font-mono);
} }

View File

@@ -8,22 +8,15 @@ if (!email) {
process.exit(1) process.exit(1)
} }
const authData = await printTable("Auth", (tx) => const authData = await printTable("Auth", (tx) => tx.select().from(AuthTable).where(eq(AuthTable.subject, email)))
tx.select().from(AuthTable).where(eq(AuthTable.subject, email)),
)
if (authData.length === 0) { if (authData.length === 0) {
console.error("User not found") console.error("User not found")
process.exit(1) process.exit(1)
} }
await printTable("Auth", (tx) => await printTable("Auth", (tx) => tx.select().from(AuthTable).where(eq(AuthTable.accountID, authData[0].accountID)))
tx.select().from(AuthTable).where(eq(AuthTable.accountID, authData[0].accountID)),
)
function printTable( function printTable(title: string, callback: (tx: Database.TxOrDb) => Promise<any[]>): Promise<any[]> {
title: string,
callback: (tx: Database.TxOrDb) => Promise<any[]>,
): Promise<any[]> {
return Database.use(async (tx) => { return Database.use(async (tx) => {
const data = await callback(tx) const data = await callback(tx)
console.log(`== ${title} ==`) console.log(`== ${title} ==`)

View File

@@ -8,14 +8,6 @@ import { KeyTable } from "../src/schema/key.sql.js"
if (Resource.App.stage !== "frank") throw new Error("This script is only for frank") if (Resource.App.stage !== "frank") throw new Error("This script is only for frank")
for (const table of [ for (const table of [AccountTable, BillingTable, KeyTable, PaymentTable, UsageTable, UserTable, WorkspaceTable]) {
AccountTable,
BillingTable,
KeyTable,
PaymentTable,
UsageTable,
UserTable,
WorkspaceTable,
]) {
await Database.use((tx) => tx.delete(table)) await Database.use((tx) => tx.delete(table))
} }

View File

@@ -24,40 +24,37 @@ export namespace AWS {
body: z.string(), body: z.string(),
}), }),
async (input) => { async (input) => {
const res = await createClient().fetch( const res = await createClient().fetch("https://email.us-east-1.amazonaws.com/v2/email/outbound-emails", {
"https://email.us-east-1.amazonaws.com/v2/email/outbound-emails", method: "POST",
{ headers: {
method: "POST", "X-Amz-Target": "SES.SendEmail",
headers: { "Content-Type": "application/json",
"X-Amz-Target": "SES.SendEmail", },
"Content-Type": "application/json", body: JSON.stringify({
FromEmailAddress: `OpenCode Zen <contact@anoma.ly>`,
Destination: {
ToAddresses: [input.to],
}, },
body: JSON.stringify({ Content: {
FromEmailAddress: `OpenCode Zen <contact@anoma.ly>`, Simple: {
Destination: { Subject: {
ToAddresses: [input.to], Charset: "UTF-8",
}, Data: input.subject,
Content: { },
Simple: { Body: {
Subject: { Text: {
Charset: "UTF-8", Charset: "UTF-8",
Data: input.subject, Data: input.body,
}, },
Body: { Html: {
Text: { Charset: "UTF-8",
Charset: "UTF-8", Data: input.body,
Data: input.body,
},
Html: {
Charset: "UTF-8",
Data: input.body,
},
}, },
}, },
}, },
}), },
}, }),
) })
if (!res.ok) { if (!res.ok) {
throw new Error(`Failed to send email: ${res.statusText}`) throw new Error(`Failed to send email: ${res.statusText}`)
} }

View File

@@ -5,10 +5,7 @@ import { Client } from "@planetscale/database"
import { MySqlTransaction, type MySqlTransactionConfig } from "drizzle-orm/mysql-core" import { MySqlTransaction, type MySqlTransactionConfig } from "drizzle-orm/mysql-core"
import type { ExtractTablesWithRelations } from "drizzle-orm" import type { ExtractTablesWithRelations } from "drizzle-orm"
import type { import type { PlanetScalePreparedQueryHKT, PlanetscaleQueryResultHKT } from "drizzle-orm/planetscale-serverless"
PlanetScalePreparedQueryHKT,
PlanetscaleQueryResultHKT,
} from "drizzle-orm/planetscale-serverless"
import { Context } from "../context" import { Context } from "../context"
import { memo } from "../util/memo" import { memo } from "../util/memo"
@@ -70,10 +67,7 @@ export namespace Database {
} }
} }
export async function transaction<T>( export async function transaction<T>(callback: (tx: TxOrDb) => Promise<T>, config?: MySqlTransactionConfig) {
callback: (tx: TxOrDb) => Promise<T>,
config?: MySqlTransactionConfig,
) {
try { try {
const { tx } = TransactionContext.use() const { tx } = TransactionContext.use()
return callback(tx) return callback(tx)

View File

@@ -20,14 +20,8 @@ export namespace Key {
email: AuthTable.subject, email: AuthTable.subject,
}) })
.from(KeyTable) .from(KeyTable)
.innerJoin( .innerJoin(UserTable, and(eq(KeyTable.userID, UserTable.id), eq(KeyTable.workspaceID, UserTable.workspaceID)))
UserTable, .innerJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")))
and(eq(KeyTable.userID, UserTable.id), eq(KeyTable.workspaceID, UserTable.workspaceID)),
)
.innerJoin(
AuthTable,
and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")),
)
.where( .where(
and( and(
...[ ...[

View File

@@ -60,9 +60,7 @@ export namespace Model {
export const enable = fn(z.object({ model: z.string() }), ({ model }) => { export const enable = fn(z.object({ model: z.string() }), ({ model }) => {
Actor.assertAdmin() Actor.assertAdmin()
return Database.use((db) => return Database.use((db) =>
db db.delete(ModelTable).where(and(eq(ModelTable.workspaceID, Actor.workspace()), eq(ModelTable.model, model))),
.delete(ModelTable)
.where(and(eq(ModelTable.workspaceID, Actor.workspace()), eq(ModelTable.model, model))),
) )
}) })

View File

@@ -11,9 +11,7 @@ export namespace Provider {
tx tx
.select() .select()
.from(ProviderTable) .from(ProviderTable)
.where( .where(and(eq(ProviderTable.workspaceID, Actor.workspace()), isNull(ProviderTable.timeDeleted))),
and(eq(ProviderTable.workspaceID, Actor.workspace()), isNull(ProviderTable.timeDeleted)),
),
), ),
) )
@@ -52,12 +50,7 @@ export namespace Provider {
return Database.transaction((tx) => return Database.transaction((tx) =>
tx tx
.delete(ProviderTable) .delete(ProviderTable)
.where( .where(and(eq(ProviderTable.provider, provider), eq(ProviderTable.workspaceID, Actor.workspace()))),
and(
eq(ProviderTable.provider, provider),
eq(ProviderTable.workspaceID, Actor.workspace()),
),
),
) )
}, },
) )

View File

@@ -1,11 +1,4 @@
import { import { index, mysqlEnum, mysqlTable, primaryKey, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
index,
mysqlEnum,
mysqlTable,
primaryKey,
uniqueIndex,
varchar,
} from "drizzle-orm/mysql-core"
import { id, timestamps, ulid } from "../drizzle/types" import { id, timestamps, ulid } from "../drizzle/types"
export const AuthProvider = ["email", "github", "google"] as const export const AuthProvider = ["email", "github", "google"] as const

View File

@@ -9,8 +9,5 @@ export const ModelTable = mysqlTable(
...timestamps, ...timestamps,
model: varchar("model", { length: 64 }).notNull(), model: varchar("model", { length: 64 }).notNull(),
}, },
(table) => [ (table) => [...workspaceIndexes(table), uniqueIndex("model_workspace_model").on(table.workspaceID, table.model)],
...workspaceIndexes(table),
uniqueIndex("model_workspace_model").on(table.workspaceID, table.model),
],
) )

View File

@@ -10,8 +10,5 @@ export const ProviderTable = mysqlTable(
provider: varchar("provider", { length: 64 }).notNull(), provider: varchar("provider", { length: 64 }).notNull(),
credentials: text("credentials").notNull(), credentials: text("credentials").notNull(),
}, },
(table) => [ (table) => [...workspaceIndexes(table), uniqueIndex("workspace_provider").on(table.workspaceID, table.provider)],
...workspaceIndexes(table),
uniqueIndex("workspace_provider").on(table.workspaceID, table.provider),
],
) )

View File

@@ -1,12 +1,4 @@
import { import { mysqlTable, uniqueIndex, varchar, int, mysqlEnum, index, bigint } from "drizzle-orm/mysql-core"
mysqlTable,
uniqueIndex,
varchar,
int,
mysqlEnum,
index,
bigint,
} from "drizzle-orm/mysql-core"
import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types" import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types"
import { workspaceIndexes } from "./workspace.sql" import { workspaceIndexes } from "./workspace.sql"

View File

@@ -26,10 +26,7 @@ export namespace User {
authEmail: AuthTable.subject, authEmail: AuthTable.subject,
}) })
.from(UserTable) .from(UserTable)
.leftJoin( .leftJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")))
AuthTable,
and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")),
)
.where(and(eq(UserTable.workspaceID, Actor.workspace()), isNull(UserTable.timeDeleted))), .where(and(eq(UserTable.workspaceID, Actor.workspace()), isNull(UserTable.timeDeleted))),
), ),
) )
@@ -39,13 +36,7 @@ export namespace User {
tx tx
.select() .select()
.from(UserTable) .from(UserTable)
.where( .where(and(eq(UserTable.workspaceID, Actor.workspace()), eq(UserTable.id, id), isNull(UserTable.timeDeleted)))
and(
eq(UserTable.workspaceID, Actor.workspace()),
eq(UserTable.id, id),
isNull(UserTable.timeDeleted),
),
)
.then((rows) => rows[0]), .then((rows) => rows[0]),
), ),
) )
@@ -57,10 +48,7 @@ export namespace User {
email: AuthTable.subject, email: AuthTable.subject,
}) })
.from(UserTable) .from(UserTable)
.leftJoin( .leftJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")))
AuthTable,
and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")),
)
.where(and(eq(UserTable.workspaceID, Actor.workspace()), eq(UserTable.id, id))) .where(and(eq(UserTable.workspaceID, Actor.workspace()), eq(UserTable.id, id)))
.then((rows) => rows[0]?.email), .then((rows) => rows[0]?.email),
), ),
@@ -142,16 +130,10 @@ export namespace User {
workspaceName: WorkspaceTable.name, workspaceName: WorkspaceTable.name,
}) })
.from(UserTable) .from(UserTable)
.innerJoin( .innerJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")))
AuthTable,
and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")),
)
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, workspaceID)) .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, workspaceID))
.where( .where(
and( and(eq(UserTable.workspaceID, workspaceID), eq(UserTable.id, Actor.assert("user").properties.userID)),
eq(UserTable.workspaceID, workspaceID),
eq(UserTable.id, Actor.assert("user").properties.userID),
),
) )
.then((rows) => rows[0]), .then((rows) => rows[0]),
) )

View File

@@ -1,18 +1,6 @@
// @ts-nocheck // @ts-nocheck
import React from "react" import React from "react"
import { import { Img, Row, Html, Link, Body, Head, Button, Column, Preview, Section, Container } from "@jsx-email/all"
Img,
Row,
Html,
Link,
Body,
Head,
Button,
Column,
Preview,
Section,
Container,
} from "@jsx-email/all"
import { Text, Fonts, Title, A, Span } from "../components" import { Text, Fonts, Title, A, Span } from "../components"
import { import {
unit, unit,
@@ -64,8 +52,8 @@ export const InviteEmail = ({
<Section style={{ padding: `${unit * 2}px 0 0 0` }}> <Section style={{ padding: `${unit * 2}px 0 0 0` }}>
<Text style={headingText}>Join your team's OpenCode workspace</Text> <Text style={headingText}>Join your team's OpenCode workspace</Text>
<Text style={contentText}> <Text style={contentText}>
You have been invited by <Span style={contentHighlightText}>{inviter}</Span> to join You have been invited by <Span style={contentHighlightText}>{inviter}</Span> to join the{" "}
the <Span style={contentHighlightText}>{workspaceName}</Span> workspace on OpenCode. <Span style={contentHighlightText}>{workspaceName}</Span> workspace on OpenCode.
</Text> </Text>
</Section> </Section>
@@ -73,12 +61,7 @@ export const InviteEmail = ({
<Button style={button} href={url}> <Button style={button} href={url}>
<Text style={buttonText}> <Text style={buttonText}>
Join workspace Join workspace
<Img <Img width="24" height="24" src={`${assetsUrl}/right-arrow.png`} alt="Arrow right" />
width="24"
height="24"
src={`${assetsUrl}/right-arrow.png`}
alt="Arrow right"
/>
</Text> </Text>
</Button> </Button>
</Section> </Section>

View File

@@ -268,11 +268,7 @@ export default new Hono<{ Bindings: Env }>()
// Verify permissions // Verify permissions
const userClient = new Octokit({ auth: token }) const userClient = new Octokit({ auth: token })
const { data: repoData } = await userClient.repos.get({ owner, repo }) const { data: repoData } = await userClient.repos.get({ owner, repo })
if ( if (!repoData.permissions.admin && !repoData.permissions.push && !repoData.permissions.maintain)
!repoData.permissions.admin &&
!repoData.permissions.push &&
!repoData.permissions.maintain
)
throw new Error("User does not have write permissions") throw new Error("User does not have write permissions")
// Get installation token // Get installation token

View File

@@ -41,9 +41,7 @@ for (const [os, arch] of targets) {
const opentui = `@opentui/core-${os === "windows" ? "win32" : os}-${arch.replace("-baseline", "")}` const opentui = `@opentui/core-${os === "windows" ? "win32" : os}-${arch.replace("-baseline", "")}`
await $`mkdir -p ../../node_modules/${opentui}` await $`mkdir -p ../../node_modules/${opentui}`
await $`npm pack ${opentui}@${pkg.dependencies["@opentui/core"]}`.cwd( await $`npm pack ${opentui}@${pkg.dependencies["@opentui/core"]}`.cwd(path.join(dir, "../../node_modules"))
path.join(dir, "../../node_modules"),
)
await $`tar -xf ../../node_modules/${opentui.replace("@opentui/", "opentui-")}-*.tgz -C ../../node_modules/${opentui} --strip-components=1` await $`tar -xf ../../node_modules/${opentui.replace("@opentui/", "opentui-")}-*.tgz -C ../../node_modules/${opentui} --strip-components=1`
const watcher = `@parcel/watcher-${os === "windows" ? "win32" : os}-${arch.replace("-baseline", "")}${os === "linux" ? "-glibc" : ""}` const watcher = `@parcel/watcher-${os === "windows" ? "win32" : os}-${arch.replace("-baseline", "")}${os === "linux" ? "-glibc" : ""}`
@@ -51,9 +49,7 @@ for (const [os, arch] of targets) {
await $`npm pack ${watcher}`.cwd(path.join(dir, "../../node_modules")).quiet() await $`npm pack ${watcher}`.cwd(path.join(dir, "../../node_modules")).quiet()
await $`tar -xf ../../node_modules/${watcher.replace("@parcel/", "parcel-")}-*.tgz -C ../../node_modules/${watcher} --strip-components=1` await $`tar -xf ../../node_modules/${watcher.replace("@parcel/", "parcel-")}-*.tgz -C ../../node_modules/${watcher} --strip-components=1`
const parserWorker = fs.realpathSync( const parserWorker = fs.realpathSync(path.resolve(dir, "./node_modules/@opentui/core/parser.worker.js"))
path.resolve(dir, "./node_modules/@opentui/core/parser.worker.js"),
)
const workerPath = "./src/cli/cmd/tui/worker.ts" const workerPath = "./src/cli/cmd/tui/worker.ts"
await Bun.build({ await Bun.build({

View File

@@ -77,8 +77,7 @@ async function regenerateWindowsCmdWrappers() {
// npm_config_global is string | undefined // npm_config_global is string | undefined
// if it exists, the value is true // if it exists, the value is true
const isGlobal = const isGlobal = process.env.npm_config_global === "true" || pkgPath.includes(path.join("npm", "node_modules"))
process.env.npm_config_global === "true" || pkgPath.includes(path.join("npm", "node_modules"))
// The npm rebuild command does 2 things - Execute lifecycle scripts and rebuild bin links // The npm rebuild command does 2 things - Execute lifecycle scripts and rebuild bin links
// We want to skip lifecycle scripts to avoid infinite loops, so we use --ignore-scripts // We want to skip lifecycle scripts to avoid infinite loops, so we use --ignore-scripts
@@ -94,9 +93,7 @@ async function regenerateWindowsCmdWrappers() {
console.log("Successfully rebuilt npm bin links") console.log("Successfully rebuilt npm bin links")
} catch (error) { } catch (error) {
console.error("Error rebuilding npm links:", error.message) console.error("Error rebuilding npm links:", error.message)
console.error( console.error("npm rebuild failed. You may need to manually run: npm rebuild opencode-ai --ignore-scripts")
"npm rebuild failed. You may need to manually run: npm rebuild opencode-ai --ignore-scripts",
)
} }
} }

View File

@@ -55,18 +55,10 @@ if (!Script.preview) {
} }
// Calculate SHA values // Calculate SHA values
const arm64Sha = await $`sha256sum ./dist/opencode-linux-arm64.zip | cut -d' ' -f1` const arm64Sha = await $`sha256sum ./dist/opencode-linux-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
.text() const x64Sha = await $`sha256sum ./dist/opencode-linux-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
.then((x) => x.trim()) const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const x64Sha = await $`sha256sum ./dist/opencode-linux-x64.zip | cut -d' ' -f1` const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
.text()
.then((x) => x.trim())
const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`
.text()
.then((x) => x.trim())
const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`
.text()
.then((x) => x.trim())
const [pkgver, _subver = ""] = Script.version.split(/(-.*)/, 2) const [pkgver, _subver = ""] = Script.version.split(/(-.*)/, 2)

View File

@@ -19,23 +19,12 @@ const result = z.toJSONSchema(Config.Info, {
const schema = ctx.jsonSchema const schema = ctx.jsonSchema
// Preserve strictness: set additionalProperties: false for objects // Preserve strictness: set additionalProperties: false for objects
if ( if (schema && typeof schema === "object" && schema.type === "object" && schema.additionalProperties === undefined) {
schema &&
typeof schema === "object" &&
schema.type === "object" &&
schema.additionalProperties === undefined
) {
schema.additionalProperties = false schema.additionalProperties = false
} }
// Add examples and default descriptions for string fields with defaults // Add examples and default descriptions for string fields with defaults
if ( if (schema && typeof schema === "object" && "type" in schema && schema.type === "string" && schema?.default) {
schema &&
typeof schema === "object" &&
"type" in schema &&
schema.type === "string" &&
schema?.default
) {
if (!schema.examples) { if (!schema.examples) {
schema.examples = [schema.default] schema.examples = [schema.default]
} }

View File

@@ -199,10 +199,8 @@ export namespace ACP {
if (kind === "edit") { if (kind === "edit") {
const input = part.state.input const input = part.state.input
const filePath = const filePath = typeof input["filePath"] === "string" ? input["filePath"] : ""
typeof input["filePath"] === "string" ? input["filePath"] : "" const oldText = typeof input["oldString"] === "string" ? input["oldString"] : ""
const oldText =
typeof input["oldString"] === "string" ? input["oldString"] : ""
const newText = const newText =
typeof input["newString"] === "string" typeof input["newString"] === "string"
? input["newString"] ? input["newString"]
@@ -218,9 +216,7 @@ export namespace ACP {
} }
if (part.tool === "todowrite") { if (part.tool === "todowrite") {
const parsedTodos = z const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output))
.array(Todo.Info)
.safeParse(JSON.parse(part.state.output))
if (parsedTodos.success) { if (parsedTodos.success) {
await this.connection await this.connection
.sessionUpdate({ .sessionUpdate({
@@ -229,9 +225,7 @@ export namespace ACP {
sessionUpdate: "plan", sessionUpdate: "plan",
entries: parsedTodos.data.map((todo) => { entries: parsedTodos.data.map((todo) => {
const status: PlanEntry["status"] = const status: PlanEntry["status"] =
todo.status === "cancelled" todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"])
? "completed"
: (todo.status as PlanEntry["status"])
return { return {
priority: "medium", priority: "medium",
status, status,
@@ -481,8 +475,7 @@ export namespace ACP {
description: agent.description, description: agent.description,
})) }))
const currentModeId = const currentModeId = availableModes.find((m) => m.name === "build")?.id ?? availableModes[0].id
availableModes.find((m) => m.name === "build")?.id ?? availableModes[0].id
const mcpServers: Record<string, Config.Mcp> = {} const mcpServers: Record<string, Config.Mcp> = {}
for (const server of params.mcpServers) { for (const server of params.mcpServers) {
@@ -587,8 +580,7 @@ export namespace ACP {
const agent = session.modeId ?? "build" const agent = session.modeId ?? "build"
const parts: Array< const parts: Array<
| { type: "text"; text: string } { type: "text"; text: string } | { type: "file"; url: string; filename: string; mime: string }
| { type: "file"; url: string; filename: string; mime: string }
> = [] > = []
for (const part of params.prompt) { for (const part of params.prompt) {
switch (part.type) { switch (part.type) {
@@ -794,9 +786,7 @@ export namespace ACP {
function parseUri( function parseUri(
uri: string, uri: string,
): ): { type: "file"; url: string; filename: string; mime: string } | { type: "text"; text: string } {
| { type: "file"; url: string; filename: string; mime: string }
| { type: "text"; text: string } {
try { try {
if (uri.startsWith("file://")) { if (uri.startsWith("file://")) {
const path = uri.slice(7) const path = uri.slice(7)

View File

@@ -13,11 +13,7 @@ export class ACPSessionManager {
this.sdk = sdk this.sdk = sdk
} }
async create( async create(cwd: string, mcpServers: McpServer[], model?: ACPSessionState["model"]): Promise<ACPSessionState> {
cwd: string,
mcpServers: McpServer[],
model?: ACPSessionState["model"],
): Promise<ACPSessionState> {
const session = await this.sdk.session const session = await this.sdk.session
.create({ .create({
body: { body: {

View File

@@ -143,18 +143,7 @@ export namespace Agent {
tools: {}, tools: {},
builtIn: false, builtIn: false,
} }
const { const { name, model, prompt, tools, description, temperature, top_p, mode, permission, ...extra } = value
name,
model,
prompt,
tools,
description,
temperature,
top_p,
mode,
permission,
...extra
} = value
item.options = { item.options = {
...item.options, ...item.options,
...extra, ...extra,
@@ -223,10 +212,7 @@ export namespace Agent {
} }
} }
function mergeAgentPermissions( function mergeAgentPermissions(basePermission: any, overridePermission: any): Agent.Info["permission"] {
basePermission: any,
overridePermission: any,
): Agent.Info["permission"] {
if (typeof basePermission.bash === "string") { if (typeof basePermission.bash === "string") {
basePermission.bash = { basePermission.bash = {
"*": basePermission.bash, "*": basePermission.bash,

View File

@@ -8,10 +8,7 @@ import { readableStreamToText } from "bun"
export namespace BunProc { export namespace BunProc {
const log = Log.create({ service: "bun" }) const log = Log.create({ service: "bun" })
export async function run( export async function run(cmd: string[], options?: Bun.SpawnOptions.OptionsObject<any, any, any>) {
cmd: string[],
options?: Bun.SpawnOptions.OptionsObject<any, any, any>,
) {
log.info("running", { log.info("running", {
cmd: [which(), ...cmd], cmd: [which(), ...cmd],
...options, ...options,

View File

@@ -19,10 +19,7 @@ export namespace Bus {
const registry = new Map<string, EventDefinition>() const registry = new Map<string, EventDefinition>()
export function event<Type extends string, Properties extends ZodType>( export function event<Type extends string, Properties extends ZodType>(type: Type, properties: Properties) {
type: Type,
properties: Properties,
) {
const result = { const result = {
type, type,
properties, properties,
@@ -73,10 +70,7 @@ export namespace Bus {
export function subscribe<Definition extends EventDefinition>( export function subscribe<Definition extends EventDefinition>(
def: Definition, def: Definition,
callback: (event: { callback: (event: { type: Definition["type"]; properties: z.infer<Definition["properties"]> }) => void,
type: Definition["type"]
properties: z.infer<Definition["properties"]>
}) => void,
) { ) {
return raw(def.type, callback) return raw(def.type, callback)
} }

View File

@@ -14,11 +14,7 @@ export const AuthCommand = cmd({
command: "auth", command: "auth",
describe: "manage credentials", describe: "manage credentials",
builder: (yargs) => builder: (yargs) =>
yargs yargs.command(AuthLoginCommand).command(AuthLogoutCommand).command(AuthListCommand).demandCommand(),
.command(AuthLoginCommand)
.command(AuthLogoutCommand)
.command(AuthListCommand)
.demandCommand(),
async handler() {}, async handler() {},
}) })
@@ -64,9 +60,7 @@ export const AuthListCommand = cmd({
prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`) prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`)
} }
prompts.outro( prompts.outro(`${activeEnvVars.length} environment variable` + (activeEnvVars.length === 1 ? "" : "s"))
`${activeEnvVars.length} environment variable` + (activeEnvVars.length === 1 ? "" : "s"),
)
} }
}, },
}) })
@@ -86,9 +80,7 @@ export const AuthLoginCommand = cmd({
UI.empty() UI.empty()
prompts.intro("Add credential") prompts.intro("Add credential")
if (args.url) { if (args.url) {
const wellknown = await fetch(`${args.url}/.well-known/opencode`).then( const wellknown = await fetch(`${args.url}/.well-known/opencode`).then((x) => x.json() as any)
(x) => x.json() as any,
)
prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``) prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
const proc = Bun.spawn({ const proc = Bun.spawn({
cmd: wellknown.auth.command, cmd: wellknown.auth.command,
@@ -290,8 +282,7 @@ export const AuthLoginCommand = cmd({
if (provider === "other") { if (provider === "other") {
provider = await prompts.text({ provider = await prompts.text({
message: "Enter provider id", message: "Enter provider id",
validate: (x) => validate: (x) => (x && x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
x && x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only",
}) })
if (prompts.isCancel(provider)) throw new UI.CancelledError() if (prompts.isCancel(provider)) throw new UI.CancelledError()
provider = provider.replace(/^@ai-sdk\//, "") provider = provider.replace(/^@ai-sdk\//, "")

View File

@@ -7,11 +7,7 @@ import { EOL } from "os"
export const LSPCommand = cmd({ export const LSPCommand = cmd({
command: "lsp", command: "lsp",
builder: (yargs) => builder: (yargs) =>
yargs yargs.command(DiagnosticsCommand).command(SymbolsCommand).command(DocumentSymbolsCommand).demandCommand(),
.command(DiagnosticsCommand)
.command(SymbolsCommand)
.command(DocumentSymbolsCommand)
.demandCommand(),
async handler() {}, async handler() {},
}) })

View File

@@ -6,8 +6,7 @@ import { cmd } from "../cmd"
export const RipgrepCommand = cmd({ export const RipgrepCommand = cmd({
command: "rg", command: "rg",
builder: (yargs) => builder: (yargs) => yargs.command(TreeCommand).command(FilesCommand).command(SearchCommand).demandCommand(),
yargs.command(TreeCommand).command(FilesCommand).command(SearchCommand).demandCommand(),
async handler() {}, async handler() {},
}) })
@@ -19,9 +18,7 @@ const TreeCommand = cmd({
}), }),
async handler(args) { async handler(args) {
await bootstrap(process.cwd(), async () => { await bootstrap(process.cwd(), async () => {
process.stdout.write( process.stdout.write((await Ripgrep.tree({ cwd: Instance.directory, limit: args.limit })) + EOL)
(await Ripgrep.tree({ cwd: Instance.directory, limit: args.limit })) + EOL,
)
}) })
}, },
}) })

View File

@@ -4,8 +4,7 @@ import { cmd } from "../cmd"
export const SnapshotCommand = cmd({ export const SnapshotCommand = cmd({
command: "snapshot", command: "snapshot",
builder: (yargs) => builder: (yargs) => yargs.command(TrackCommand).command(PatchCommand).command(DiffCommand).demandCommand(),
yargs.command(TrackCommand).command(PatchCommand).command(DiffCommand).demandCommand(),
async handler() {}, async handler() {},
}) })

View File

@@ -189,9 +189,7 @@ export const GithubInstallCommand = cmd({
async function getAppInfo() { async function getAppInfo() {
const project = Instance.project const project = Instance.project
if (project.vcs !== "git") { if (project.vcs !== "git") {
prompts.log.error( prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
`Could not find git repository. Please run this command from a git repository.`,
)
throw new UI.CancelledError() throw new UI.CancelledError()
} }
@@ -204,13 +202,9 @@ export const GithubInstallCommand = cmd({
// ie. git@github.com:sst/opencode // ie. git@github.com:sst/opencode
// ie. ssh://git@github.com/sst/opencode.git // ie. ssh://git@github.com/sst/opencode.git
// ie. ssh://git@github.com/sst/opencode // ie. ssh://git@github.com/sst/opencode
const parsed = info.match( const parsed = info.match(/^(?:(?:https?|ssh):\/\/)?(?:git@)?github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/)
/^(?:(?:https?|ssh):\/\/)?(?:git@)?github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/,
)
if (!parsed) { if (!parsed) {
prompts.log.error( prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
`Could not find git repository. Please run this command from a git repository.`,
)
throw new UI.CancelledError() throw new UI.CancelledError()
} }
const [, owner, repo] = parsed const [, owner, repo] = parsed
@@ -451,9 +445,7 @@ export const GithubRunCommand = cmd({
const summary = await summarize(response) const summary = await summarize(response)
await pushToLocalBranch(summary) await pushToLocalBranch(summary)
} }
const hasShared = prData.comments.nodes.some((c) => const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`))
c.body.includes(`${shareBaseUrl}/s/${shareId}`),
)
await updateComment(`${response}${footer({ image: !hasShared })}`) await updateComment(`${response}${footer({ image: !hasShared })}`)
} }
// Fork PR // Fork PR
@@ -465,9 +457,7 @@ export const GithubRunCommand = cmd({
const summary = await summarize(response) const summary = await summarize(response)
await pushToForkBranch(summary, prData) await pushToForkBranch(summary, prData)
} }
const hasShared = prData.comments.nodes.some((c) => const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`))
c.body.includes(`${shareBaseUrl}/s/${shareId}`),
)
await updateComment(`${response}${footer({ image: !hasShared })}`) await updateComment(`${response}${footer({ image: !hasShared })}`)
} }
} }
@@ -557,12 +547,8 @@ export const GithubRunCommand = cmd({
// ie. <img alt="Image" src="https://github.com/user-attachments/assets/xxxx" /> // ie. <img alt="Image" src="https://github.com/user-attachments/assets/xxxx" />
// ie. [api.json](https://github.com/user-attachments/files/21433810/api.json) // ie. [api.json](https://github.com/user-attachments/files/21433810/api.json)
// ie. ![Image](https://github.com/user-attachments/assets/xxxx) // ie. ![Image](https://github.com/user-attachments/assets/xxxx)
const mdMatches = prompt.matchAll( const mdMatches = prompt.matchAll(/!?\[.*?\]\((https:\/\/github\.com\/user-attachments\/[^)]+)\)/gi)
/!?\[.*?\]\((https:\/\/github\.com\/user-attachments\/[^)]+)\)/gi, const tagMatches = prompt.matchAll(/<img .*?src="(https:\/\/github\.com\/user-attachments\/[^"]+)" \/>/gi)
)
const tagMatches = prompt.matchAll(
/<img .*?src="(https:\/\/github\.com\/user-attachments\/[^"]+)" \/>/gi,
)
const matches = [...mdMatches, ...tagMatches].sort((a, b) => a.index - b.index) const matches = [...mdMatches, ...tagMatches].sort((a, b) => a.index - b.index)
console.log("Images", JSON.stringify(matches, null, 2)) console.log("Images", JSON.stringify(matches, null, 2))
@@ -587,10 +573,7 @@ export const GithubRunCommand = cmd({
// Replace img tag with file path, ie. @image.png // Replace img tag with file path, ie. @image.png
const replacement = `@${filename}` const replacement = `@${filename}`
prompt = prompt = prompt.slice(0, start + offset) + replacement + prompt.slice(start + offset + tag.length)
prompt.slice(0, start + offset) +
replacement +
prompt.slice(start + offset + tag.length)
offset += replacement.length - tag.length offset += replacement.length - tag.length
const contentType = res.headers.get("content-type") const contentType = res.headers.get("content-type")
@@ -873,8 +856,7 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
throw new Error(`Failed to check permissions for user ${actor}: ${error}`) throw new Error(`Failed to check permissions for user ${actor}: ${error}`)
} }
if (!["admin", "write"].includes(permission)) if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`)
throw new Error(`User ${actor} does not have write permissions`)
} }
async function createComment() { async function createComment() {
@@ -922,9 +904,7 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
return `<a href="${shareBaseUrl}/s/${shareId}"><img width="200" alt="${titleAlt}" src="https://social-cards.sst.dev/opencode-share/${title64}.png?model=${providerID}/${modelID}&version=${session.version}&id=${shareId}" /></a>\n` return `<a href="${shareBaseUrl}/s/${shareId}"><img width="200" alt="${titleAlt}" src="https://social-cards.sst.dev/opencode-share/${title64}.png?model=${providerID}/${modelID}&version=${session.version}&id=${shareId}" /></a>\n`
})() })()
const shareUrl = shareId const shareUrl = shareId ? `[opencode session](${shareBaseUrl}/s/${shareId})&nbsp;&nbsp;|&nbsp;&nbsp;` : ""
? `[opencode session](${shareBaseUrl}/s/${shareId})&nbsp;&nbsp;|&nbsp;&nbsp;`
: ""
return `\n\n${image}${shareUrl}[github run](${runUrl})` return `\n\n${image}${shareUrl}[github run](${runUrl})`
} }
@@ -1100,13 +1080,9 @@ query($owner: String!, $repo: String!, $number: Int!) {
}) })
.map((c) => `- ${c.author.login} at ${c.createdAt}: ${c.body}`) .map((c) => `- ${c.author.login} at ${c.createdAt}: ${c.body}`)
const files = (pr.files.nodes || []).map( const files = (pr.files.nodes || []).map((f) => `- ${f.path} (${f.changeType}) +${f.additions}/-${f.deletions}`)
(f) => `- ${f.path} (${f.changeType}) +${f.additions}/-${f.deletions}`,
)
const reviewData = (pr.reviews.nodes || []).map((r) => { const reviewData = (pr.reviews.nodes || []).map((r) => {
const comments = (r.comments.nodes || []).map( const comments = (r.comments.nodes || []).map((c) => ` - ${c.path}:${c.line ?? "?"}: ${c.body}`)
(c) => ` - ${c.path}:${c.line ?? "?"}: ${c.body}`,
)
return [ return [
`- ${r.author.login} at ${r.submittedAt}:`, `- ${r.author.login} at ${r.submittedAt}:`,
` - Review body: ${r.body}`, ` - Review body: ${r.body}`,
@@ -1128,15 +1104,9 @@ query($owner: String!, $repo: String!, $number: Int!) {
`Deletions: ${pr.deletions}`, `Deletions: ${pr.deletions}`,
`Total Commits: ${pr.commits.totalCount}`, `Total Commits: ${pr.commits.totalCount}`,
`Changed Files: ${pr.files.nodes.length} files`, `Changed Files: ${pr.files.nodes.length} files`,
...(comments.length > 0 ...(comments.length > 0 ? ["<pull_request_comments>", ...comments, "</pull_request_comments>"] : []),
? ["<pull_request_comments>", ...comments, "</pull_request_comments>"] ...(files.length > 0 ? ["<pull_request_changed_files>", ...files, "</pull_request_changed_files>"] : []),
: []), ...(reviewData.length > 0 ? ["<pull_request_reviews>", ...reviewData, "</pull_request_reviews>"] : []),
...(files.length > 0
? ["<pull_request_changed_files>", ...files, "</pull_request_changed_files>"]
: []),
...(reviewData.length > 0
? ["<pull_request_reviews>", ...reviewData, "</pull_request_reviews>"]
: []),
"</pull_request>", "</pull_request>",
].join("\n") ].join("\n")
} }

View File

@@ -138,9 +138,7 @@ export const RunCommand = cmd({
const outputJsonEvent = (type: string, data: any) => { const outputJsonEvent = (type: string, data: any) => {
if (args.format === "json") { if (args.format === "json") {
process.stdout.write( process.stdout.write(JSON.stringify({ type, timestamp: Date.now(), sessionID, ...data }) + EOL)
JSON.stringify({ type, timestamp: Date.now(), sessionID, ...data }) + EOL,
)
return true return true
} }
return false return false
@@ -160,9 +158,7 @@ export const RunCommand = cmd({
const [tool, color] = TOOL[part.tool] ?? [part.tool, UI.Style.TEXT_INFO_BOLD] const [tool, color] = TOOL[part.tool] ?? [part.tool, UI.Style.TEXT_INFO_BOLD]
const title = const title =
part.state.title || part.state.title ||
(Object.keys(part.state.input).length > 0 (Object.keys(part.state.input).length > 0 ? JSON.stringify(part.state.input) : "Unknown")
? JSON.stringify(part.state.input)
: "Unknown")
printEvent(color, tool, title) printEvent(color, tool, title)
if (part.tool === "bash" && part.state.output?.trim()) { if (part.tool === "bash" && part.state.output?.trim()) {
UI.println() UI.println()
@@ -215,10 +211,7 @@ export const RunCommand = cmd({
], ],
initialValue: "once", initialValue: "once",
}).catch(() => "reject") }).catch(() => "reject")
const response = (result.toString().includes("cancel") ? "reject" : result) as const response = (result.toString().includes("cancel") ? "reject" : result) as "once" | "always" | "reject"
| "once"
| "always"
| "reject"
await sdk.postSessionIdPermissionsPermissionId({ await sdk.postSessionIdPermissionsPermissionId({
path: { id: sessionID, permissionID: permission.id }, path: { id: sessionID, permissionID: permission.id },
body: { response }, body: { response },
@@ -280,10 +273,7 @@ export const RunCommand = cmd({
} }
const cfgResult = await sdk.config.get() const cfgResult = await sdk.config.get()
if ( if (cfgResult.data && (cfgResult.data.share === "auto" || Flag.OPENCODE_AUTO_SHARE || args.share)) {
cfgResult.data &&
(cfgResult.data.share === "auto" || Flag.OPENCODE_AUTO_SHARE || args.share)
) {
const shareResult = await sdk.session.share({ path: { id: sessionID } }).catch((error) => { const shareResult = await sdk.session.share({ path: { id: sessionID } }).catch((error) => {
if (error instanceof Error && error.message.includes("disabled")) { if (error instanceof Error && error.message.includes("disabled")) {
UI.println(UI.Style.TEXT_DANGER_BOLD + "! " + error.message) UI.println(UI.Style.TEXT_DANGER_BOLD + "! " + error.message)
@@ -336,10 +326,7 @@ export const RunCommand = cmd({
} }
const cfgResult = await sdk.config.get() const cfgResult = await sdk.config.get()
if ( if (cfgResult.data && (cfgResult.data.share === "auto" || Flag.OPENCODE_AUTO_SHARE || args.share)) {
cfgResult.data &&
(cfgResult.data.share === "auto" || Flag.OPENCODE_AUTO_SHARE || args.share)
) {
const shareResult = await sdk.session.share({ path: { id: sessionID } }).catch((error) => { const shareResult = await sdk.session.share({ path: { id: sessionID } }).catch((error) => {
if (error instanceof Error && error.message.includes("disabled")) { if (error instanceof Error && error.message.includes("disabled")) {
UI.println(UI.Style.TEXT_DANGER_BOLD + "! " + error.message) UI.println(UI.Style.TEXT_DANGER_BOLD + "! " + error.message)

View File

@@ -68,9 +68,7 @@ async function getAllSessions(): Promise<Session.Info[]> {
if (!project) continue if (!project) continue
const sessionKeys = await Storage.list(["session", project.id]) const sessionKeys = await Storage.list(["session", project.id])
const projectSessions = await Promise.all( const projectSessions = await Promise.all(sessionKeys.map((key) => Storage.read<Session.Info>(key)))
sessionKeys.map((key) => Storage.read<Session.Info>(key)),
)
for (const session of projectSessions) { for (const session of projectSessions) {
if (session) { if (session) {
@@ -87,16 +85,12 @@ async function aggregateSessionStats(days?: number, projectFilter?: string): Pro
const DAYS_IN_SECOND = 24 * 60 * 60 * 1000 const DAYS_IN_SECOND = 24 * 60 * 60 * 1000
const cutoffTime = days ? Date.now() - days * DAYS_IN_SECOND : 0 const cutoffTime = days ? Date.now() - days * DAYS_IN_SECOND : 0
let filteredSessions = days let filteredSessions = days ? sessions.filter((session) => session.time.updated >= cutoffTime) : sessions
? sessions.filter((session) => session.time.updated >= cutoffTime)
: sessions
if (projectFilter !== undefined) { if (projectFilter !== undefined) {
if (projectFilter === "") { if (projectFilter === "") {
const currentProject = await getCurrentProject() const currentProject = await getCurrentProject()
filteredSessions = filteredSessions.filter( filteredSessions = filteredSessions.filter((session) => session.projectID === currentProject.id)
(session) => session.projectID === currentProject.id,
)
} else { } else {
filteredSessions = filteredSessions.filter((session) => session.projectID === projectFilter) filteredSessions = filteredSessions.filter((session) => session.projectID === projectFilter)
} }
@@ -125,9 +119,7 @@ async function aggregateSessionStats(days?: number, projectFilter?: string): Pro
} }
if (filteredSessions.length > 1000) { if (filteredSessions.length > 1000) {
console.log( console.log(`Large dataset detected (${filteredSessions.length} sessions). This may take a while...`)
`Large dataset detected (${filteredSessions.length} sessions). This may take a while...`,
)
} }
if (filteredSessions.length === 0) { if (filteredSessions.length === 0) {
@@ -262,8 +254,7 @@ export function displayStats(stats: SessionStats, toolLimit?: number) {
const percentage = ((count / totalToolUsage) * 100).toFixed(1) const percentage = ((count / totalToolUsage) * 100).toFixed(1)
const maxToolLength = 18 const maxToolLength = 18
const truncatedTool = const truncatedTool = tool.length > maxToolLength ? tool.substring(0, maxToolLength - 2) + ".." : tool
tool.length > maxToolLength ? tool.substring(0, maxToolLength - 2) + ".." : tool
const toolName = truncatedTool.padEnd(maxToolLength) const toolName = truncatedTool.padEnd(maxToolLength)
const content = ` ${toolName} ${bar.padEnd(20)} ${count.toString().padStart(3)} (${percentage.padStart(4)}%)` const content = ` ${toolName} ${bar.padEnd(20)} ${count.toString().padStart(3)} (${percentage.padStart(4)}%)`

View File

@@ -115,11 +115,7 @@ export function tui(input: {
render( render(
() => { () => {
return ( return (
<ErrorBoundary <ErrorBoundary fallback={(error, reset) => <ErrorComponent error={error} reset={reset} onExit={onExit} />}>
fallback={(error, reset) => (
<ErrorComponent error={error} reset={reset} onExit={onExit} />
)}
>
<ExitProvider onExit={onExit}> <ExitProvider onExit={onExit}>
<KVProvider> <KVProvider>
<ToastProvider> <ToastProvider>
@@ -413,12 +409,7 @@ function App() {
flexShrink={0} flexShrink={0}
> >
<box flexDirection="row"> <box flexDirection="row">
<box <box flexDirection="row" backgroundColor={theme.backgroundElement} paddingLeft={1} paddingRight={1}>
flexDirection="row"
backgroundColor={theme.backgroundElement}
paddingLeft={1}
paddingRight={1}
>
<text fg={theme.textMuted}>open</text> <text fg={theme.textMuted}>open</text>
<text fg={theme.text} attributes={TextAttributes.BOLD}> <text fg={theme.text} attributes={TextAttributes.BOLD}>
code{" "} code{" "}
@@ -434,11 +425,7 @@ function App() {
tab tab
</text> </text>
<text fg={local.agent.color(local.agent.current().name)}>{""}</text> <text fg={local.agent.color(local.agent.current().name)}>{""}</text>
<text <text bg={local.agent.color(local.agent.current().name)} fg={theme.background} wrapMode={undefined}>
bg={local.agent.color(local.agent.current().name)}
fg={theme.background}
wrapMode={undefined}
>
<span style={{ bold: true }}> {local.agent.current().name.toUpperCase()}</span> <span style={{ bold: true }}> {local.agent.current().name.toUpperCase()}</span>
<span> AGENT </span> <span> AGENT </span>
</text> </text>

View File

@@ -52,11 +52,7 @@ export function DialogModel() {
description: provider.name, description: provider.name,
category: provider.name, category: provider.name,
})), })),
filter( filter((x) => Boolean(ref()?.filter) || !local.model.recent().find((y) => isDeepEqual(y, x.value))),
(x) =>
Boolean(ref()?.filter) ||
!local.model.recent().find((y) => isDeepEqual(y, x.value)),
),
), ),
), ),
), ),

View File

@@ -20,9 +20,7 @@ export function DialogSessionList() {
const deleteKeybind = "ctrl+d" const deleteKeybind = "ctrl+d"
const currentSessionID = createMemo(() => const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
route.data.type === "session" ? route.data.sessionID : undefined,
)
const options = createMemo(() => { const options = createMemo(() => {
const today = new Date().toDateString() const today = new Date().toDateString()

View File

@@ -77,10 +77,7 @@ export function DialogStatus() {
</For> </For>
</box> </box>
)} )}
<Show <Show when={enabledFormatters().length > 0} fallback={<text fg={theme.text}>No Formatters</text>}>
when={enabledFormatters().length > 0}
fallback={<text fg={theme.text}>No Formatters</text>}
>
<box> <box>
<text fg={theme.text}>{enabledFormatters().length} Formatters</text> <text fg={theme.text}>{enabledFormatters().length} Formatters</text>
<For each={enabledFormatters()}> <For each={enabledFormatters()}>

View File

@@ -3,19 +3,9 @@ import { TextAttributes } from "@opentui/core"
import { For } from "solid-js" import { For } from "solid-js"
import { useTheme } from "@tui/context/theme" import { useTheme } from "@tui/context/theme"
const LOGO_LEFT = [ const LOGO_LEFT = [` `, `█▀▀█ █▀▀█ █▀▀█ █▀▀▄`, `█░░█ █░░█ █▀▀▀ █░░█`, `▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀`]
` `,
`█▀▀█ █▀▀█ █▀▀█ █▀▀▄`,
`█░░█ █░░█ █▀▀▀ █░░█`,
`▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀`,
]
const LOGO_RIGHT = [ const LOGO_RIGHT = [``, `█▀▀▀ █▀▀█ █▀▀█ █▀▀█`, `█░░░ █░░█ █░░█ █▀▀▀`, `▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`]
``,
`█▀▀▀ █▀▀█ █▀▀█ █▀▀█`,
`█░░░ █░░█ █░░█ █▀▀▀`,
`▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`,
]
export function Logo() { export function Logo() {
const { theme } = useTheme() const { theme } = useTheme()

View File

@@ -83,12 +83,7 @@ export function Autocomplete(props: {
const extmarkStart = store.index const extmarkStart = store.index
const extmarkEnd = extmarkStart + Bun.stringWidth(virtualText) const extmarkEnd = extmarkStart + Bun.stringWidth(virtualText)
const styleId = const styleId = part.type === "file" ? props.fileStyleId : part.type === "agent" ? props.agentStyleId : undefined
part.type === "file"
? props.fileStyleId
: part.type === "agent"
? props.agentStyleId
: undefined
const extmarkId = input.extmarks.create({ const extmarkId = input.extmarks.create({
start: extmarkStart, start: extmarkStart,
@@ -185,9 +180,7 @@ export function Autocomplete(props: {
) )
}) })
const session = createMemo(() => const session = createMemo(() => (props.sessionID ? sync.session.get(props.sessionID) : undefined))
props.sessionID ? sync.session.get(props.sessionID) : undefined,
)
const commands = createMemo((): AutocompleteOption[] => { const commands = createMemo((): AutocompleteOption[] => {
const results: AutocompleteOption[] = [] const results: AutocompleteOption[] = []
const s = session() const s = session()
@@ -324,9 +317,7 @@ export function Autocomplete(props: {
const options = createMemo(() => { const options = createMemo(() => {
const mixed: AutocompleteOption[] = ( const mixed: AutocompleteOption[] = (
store.visible === "@" store.visible === "@" ? [...agents(), ...(files.loading ? files.latest || [] : files())] : [...commands()]
? [...agents(), ...(files.loading ? files.latest || [] : files())]
: [...commands()]
).filter((x) => x.disabled !== true) ).filter((x) => x.disabled !== true)
const currentFilter = filter() const currentFilter = filter()
if (!currentFilter) return mixed.slice(0, 10) if (!currentFilter) return mixed.slice(0, 10)
@@ -393,9 +384,7 @@ export function Autocomplete(props: {
return return
} }
// Check if a space was typed after the trigger character // Check if a space was typed after the trigger character
const currentText = props const currentText = props.input().getTextRange(store.index + 1, props.input().cursorOffset + 1)
.input()
.getTextRange(store.index + 1, props.input().cursorOffset + 1)
if (currentText.includes(" ")) { if (currentText.includes(" ")) {
hide() hide()
} }
@@ -433,13 +422,8 @@ export function Autocomplete(props: {
if (e.name === "@") { if (e.name === "@") {
const cursorOffset = props.input().cursorOffset const cursorOffset = props.input().cursorOffset
const charBeforeCursor = const charBeforeCursor =
cursorOffset === 0 cursorOffset === 0 ? undefined : props.input().getTextRange(cursorOffset - 1, cursorOffset)
? undefined const canTrigger = charBeforeCursor === undefined || charBeforeCursor === "" || /\s/.test(charBeforeCursor)
: props.input().getTextRange(cursorOffset - 1, cursorOffset)
const canTrigger =
charBeforeCursor === undefined ||
charBeforeCursor === "" ||
/\s/.test(charBeforeCursor)
if (canTrigger) show("@") if (canTrigger) show("@")
} }
@@ -487,10 +471,7 @@ export function Autocomplete(props: {
{option.display} {option.display}
</text> </text>
<Show when={option.description}> <Show when={option.description}>
<text <text fg={index() === store.selected ? theme.background : theme.textMuted} wrapMode="none">
fg={index() === store.selected ? theme.background : theme.textMuted}
wrapMode="none"
>
{option.description} {option.description}
</text> </text>
</Show> </Show>

View File

@@ -334,9 +334,7 @@ export function Prompt(props: PromptProps) {
// Expand pasted text inline before submitting // Expand pasted text inline before submitting
const allExtmarks = input.extmarks.getAllForTypeId(promptPartTypeId) const allExtmarks = input.extmarks.getAllForTypeId(promptPartTypeId)
const sortedExtmarks = allExtmarks.sort( const sortedExtmarks = allExtmarks.sort((a: { start: number }, b: { start: number }) => b.start - a.start)
(a: { start: number }, b: { start: number }) => b.start - a.start,
)
for (const extmark of sortedExtmarks) { for (const extmark of sortedExtmarks) {
const partIndex = store.extmarkToPartIndex.get(extmark.id) const partIndex = store.extmarkToPartIndex.get(extmark.id)
@@ -499,28 +497,15 @@ export function Prompt(props: PromptProps) {
<box <box
flexDirection="row" flexDirection="row"
{...SplitBorder} {...SplitBorder}
borderColor={ borderColor={keybind.leader ? theme.accent : store.mode === "shell" ? theme.secondary : theme.border}
keybind.leader ? theme.accent : store.mode === "shell" ? theme.secondary : theme.border
}
justifyContent="space-evenly" justifyContent="space-evenly"
> >
<box <box backgroundColor={theme.backgroundElement} width={3} height="100%" alignItems="center" paddingTop={1}>
backgroundColor={theme.backgroundElement}
width={3}
height="100%"
alignItems="center"
paddingTop={1}
>
<text attributes={TextAttributes.BOLD} fg={theme.primary}> <text attributes={TextAttributes.BOLD} fg={theme.primary}>
{store.mode === "normal" ? ">" : "!"} {store.mode === "normal" ? ">" : "!"}
</text> </text>
</box> </box>
<box <box paddingTop={1} paddingBottom={1} backgroundColor={theme.backgroundElement} flexGrow={1}>
paddingTop={1}
paddingBottom={1}
backgroundColor={theme.backgroundElement}
flexGrow={1}
>
<textarea <textarea
placeholder={ placeholder={
props.showPlaceholder props.showPlaceholder
@@ -575,10 +560,7 @@ export function Prompt(props: PromptProps) {
return return
} }
if (store.mode === "shell") { if (store.mode === "shell") {
if ( if ((e.name === "backspace" && input.visualCursor.offset === 0) || e.name === "escape") {
(e.name === "backspace" && input.visualCursor.offset === 0) ||
e.name === "escape"
) {
setStore("mode", "normal") setStore("mode", "normal")
e.preventDefault() e.preventDefault()
return return
@@ -588,8 +570,7 @@ export function Prompt(props: PromptProps) {
if (!autocomplete.visible) { if (!autocomplete.visible) {
if ( if (
(keybind.match("history_previous", e) && input.cursorOffset === 0) || (keybind.match("history_previous", e) && input.cursorOffset === 0) ||
(keybind.match("history_next", e) && (keybind.match("history_next", e) && input.cursorOffset === input.plainText.length)
input.cursorOffset === input.plainText.length)
) { ) {
const direction = keybind.match("history_previous", e) ? -1 : 1 const direction = keybind.match("history_previous", e) ? -1 : 1
const item = history.move(direction, input.plainText) const item = history.move(direction, input.plainText)
@@ -605,12 +586,8 @@ export function Prompt(props: PromptProps) {
return return
} }
if (keybind.match("history_previous", e) && input.visualCursor.visualRow === 0) if (keybind.match("history_previous", e) && input.visualCursor.visualRow === 0) input.cursorOffset = 0
input.cursorOffset = 0 if (keybind.match("history_next", e) && input.visualCursor.visualRow === input.height - 1)
if (
keybind.match("history_next", e) &&
input.visualCursor.visualRow === input.height - 1
)
input.cursorOffset = input.plainText.length input.cursorOffset = input.plainText.length
} }
}} }}
@@ -701,12 +678,7 @@ export function Prompt(props: PromptProps) {
syntaxStyle={syntax()} syntaxStyle={syntax()}
/> />
</box> </box>
<box <box backgroundColor={theme.backgroundElement} width={1} justifyContent="center" alignItems="center"></box>
backgroundColor={theme.backgroundElement}
width={1}
justifyContent="center"
alignItems="center"
></box>
</box> </box>
<box flexDirection="row" justifyContent="space-between"> <box flexDirection="row" justifyContent="space-between">
<text flexShrink={0} wrapMode="none" fg={theme.text}> <text flexShrink={0} wrapMode="none" fg={theme.text}>
@@ -727,8 +699,7 @@ export function Prompt(props: PromptProps) {
<Match when={props.hint}>{props.hint!}</Match> <Match when={props.hint}>{props.hint!}</Match>
<Match when={true}> <Match when={true}>
<text fg={theme.text}> <text fg={theme.text}>
{keybind.print("command_list")}{" "} {keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span>
<span style={{ fg: theme.textMuted }}>commands</span>
</text> </text>
</Match> </Match>
</Switch> </Switch>

View File

@@ -22,9 +22,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
return !!provider?.models[model.modelID] return !!provider?.models[model.modelID]
} }
function getFirstValidModel( function getFirstValidModel(...modelFns: (() => { providerID: string; modelID: string } | undefined)[]) {
...modelFns: (() => { providerID: string; modelID: string } | undefined)[]
) {
for (const modelFn of modelFns) { for (const modelFn of modelFns) {
const model = modelFn() const model = modelFn()
if (!model) continue if (!model) continue
@@ -213,9 +211,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const current = currentModel() const current = currentModel()
if (!current) return if (!current) return
const recent = modelStore.recent const recent = modelStore.recent
const index = recent.findIndex( const index = recent.findIndex((x) => x.providerID === current.providerID && x.modelID === current.modelID)
(x) => x.providerID === current.providerID && x.modelID === current.modelID,
)
if (index === -1) return if (index === -1) return
let next = index + direction let next = index + direction
if (next < 0) next = recent.length - 1 if (next < 0) next = recent.length - 1

View File

@@ -146,12 +146,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
} }
const result = Binary.search(messages, event.properties.info.id, (m) => m.id) const result = Binary.search(messages, event.properties.info.id, (m) => m.id)
if (result.found) { if (result.found) {
setStore( setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info))
"message",
event.properties.info.sessionID,
result.index,
reconcile(event.properties.info),
)
break break
} }
setStore( setStore(
@@ -186,12 +181,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
} }
const result = Binary.search(parts, event.properties.part.id, (p) => p.id) const result = Binary.search(parts, event.properties.part.id, (p) => p.id)
if (result.found) { if (result.found) {
setStore( setStore("part", event.properties.part.messageID, result.index, reconcile(event.properties.part))
"part",
event.properties.part.messageID,
result.index,
reconcile(event.properties.part),
)
break break
} }
setStore( setStore(

View File

@@ -528,13 +528,7 @@ function generateSyntax(theme: Theme) {
}, },
}, },
{ {
scope: [ scope: ["variable.builtin", "type.builtin", "function.builtin", "module.builtin", "constant.builtin"],
"variable.builtin",
"type.builtin",
"function.builtin",
"module.builtin",
"constant.builtin",
],
style: { style: {
foreground: theme.error, foreground: theme.error,
}, },

View File

@@ -26,11 +26,7 @@ export function Home() {
</Match> </Match>
<Match when={true}> <Match when={true}>
<span style={{ fg: theme.success }}></span>{" "} <span style={{ fg: theme.success }}></span>{" "}
{Locale.pluralize( {Locale.pluralize(Object.values(sync.data.mcp).length, "{} mcp server", "{} mcp servers")}
Object.values(sync.data.mcp).length,
"{} mcp server",
"{} mcp servers",
)}
</Match> </Match>
</Switch> </Switch>
</text> </text>
@@ -39,14 +35,7 @@ export function Home() {
) )
return ( return (
<box <box flexGrow={1} justifyContent="center" alignItems="center" paddingLeft={2} paddingRight={2} gap={1}>
flexGrow={1}
justifyContent="center"
alignItems="center"
paddingLeft={2}
paddingRight={2}
gap={1}
>
<Logo /> <Logo />
<box width={39}> <box width={39}>
<HelpRow keybind="command_list">Commands</HelpRow> <HelpRow keybind="command_list">Commands</HelpRow>

View File

@@ -7,9 +7,7 @@ import { useRoute } from "@tui/context/route"
export function DialogMessage(props: { messageID: string; sessionID: string }) { export function DialogMessage(props: { messageID: string; sessionID: string }) {
const sync = useSync() const sync = useSync()
const sdk = useSDK() const sdk = useSDK()
const message = createMemo(() => const message = createMemo(() => sync.data.message[props.sessionID]?.find((x) => x.id === props.messageID))
sync.data.message[props.sessionID]?.find((x) => x.id === props.messageID),
)
const route = useRoute() const route = useRoute()
return ( return (

View File

@@ -19,9 +19,7 @@ export function DialogTimeline(props: { sessionID: string; onMove: (messageID: s
const result = [] as DialogSelectOption<string>[] const result = [] as DialogSelectOption<string>[]
for (const message of messages) { for (const message of messages) {
if (message.role !== "user") continue if (message.role !== "user") continue
const part = (sync.data.part[message.id] ?? []).find( const part = (sync.data.part[message.id] ?? []).find((x) => x.type === "text" && !x.synthetic) as TextPart
(x) => x.type === "text" && !x.synthetic,
) as TextPart
if (!part) continue if (!part) continue
result.push({ result.push({
title: part.text.replace(/\n/g, " "), title: part.text.replace(/\n/g, " "),
@@ -35,11 +33,5 @@ export function DialogTimeline(props: { sessionID: string; onMove: (messageID: s
return result return result
}) })
return ( return <DialogSelect onMove={(option) => props.onMove(option.value)} title="Timeline" options={options()} />
<DialogSelect
onMove={(option) => props.onMove(option.value)}
title="Timeline"
options={options()}
/>
)
} }

View File

@@ -46,16 +46,10 @@ export function Header() {
}) })
const context = createMemo(() => { const context = createMemo(() => {
const last = messages().findLast( const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage
(x) => x.role === "assistant" && x.tokens.output > 0,
) as AssistantMessage
if (!last) return if (!last) return
const total = const total =
last.tokens.input + last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
last.tokens.output +
last.tokens.reasoning +
last.tokens.cache.read +
last.tokens.cache.write
const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID] const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID]
let result = total.toLocaleString() let result = total.toLocaleString()
if (model?.limit.context) { if (model?.limit.context) {
@@ -67,13 +61,7 @@ export function Header() {
const { theme } = useTheme() const { theme } = useTheme()
return ( return (
<box <box paddingLeft={1} paddingRight={1} {...SplitBorder} borderColor={theme.backgroundElement} flexShrink={0}>
paddingLeft={1}
paddingRight={1}
{...SplitBorder}
borderColor={theme.backgroundElement}
flexShrink={0}
>
<Show <Show
when={shareEnabled()} when={shareEnabled()}
fallback={ fallback={

View File

@@ -19,14 +19,7 @@ import { SplitBorder } from "@tui/component/border"
import { useTheme } from "@tui/context/theme" import { useTheme } from "@tui/context/theme"
import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers } from "@opentui/core" import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers } from "@opentui/core"
import { Prompt, type PromptRef } from "@tui/component/prompt" import { Prompt, type PromptRef } from "@tui/component/prompt"
import type { import type { AssistantMessage, Part, ToolPart, UserMessage, TextPart, ReasoningPart } from "@opencode-ai/sdk"
AssistantMessage,
Part,
ToolPart,
UserMessage,
TextPart,
ReasoningPart,
} from "@opencode-ai/sdk"
import { useLocal } from "@tui/context/local" import { useLocal } from "@tui/context/local"
import { Locale } from "@/util/locale" import { Locale } from "@/util/locale"
import type { Tool } from "@/tool/tool" import type { Tool } from "@/tool/tool"
@@ -41,13 +34,7 @@ import type { EditTool } from "@/tool/edit"
import type { PatchTool } from "@/tool/patch" import type { PatchTool } from "@/tool/patch"
import type { WebFetchTool } from "@/tool/webfetch" import type { WebFetchTool } from "@/tool/webfetch"
import type { TaskTool } from "@/tool/task" import type { TaskTool } from "@/tool/task"
import { import { useKeyboard, useRenderer, useTerminalDimensions, type BoxProps, type JSX } from "@opentui/solid"
useKeyboard,
useRenderer,
useTerminalDimensions,
type BoxProps,
type JSX,
} from "@opentui/solid"
import { useSDK } from "@tui/context/sdk" import { useSDK } from "@tui/context/sdk"
import { useCommandDialog } from "@tui/component/dialog-command" import { useCommandDialog } from "@tui/component/dialog-command"
import { Shimmer } from "@tui/ui/shimmer" import { Shimmer } from "@tui/ui/shimmer"
@@ -653,14 +640,7 @@ export function Session() {
conceal, conceal,
}} }}
> >
<box <box flexDirection="row" paddingBottom={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={2}>
flexDirection="row"
paddingBottom={1}
paddingTop={1}
paddingLeft={2}
paddingRight={2}
gap={2}
>
<box flexGrow={1} gap={1}> <box flexGrow={1} gap={1}>
<Show when={session()}> <Show when={session()}>
<Show when={session().parentID}> <Show when={session().parentID}>
@@ -675,19 +655,13 @@ export function Session() {
paddingRight={2} paddingRight={2}
> >
<text fg={theme.text}> <text fg={theme.text}>
Previous{" "} Previous <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle_reverse")}</span>
<span style={{ fg: theme.textMuted }}>
{keybind.print("session_child_cycle_reverse")}
</span>
</text> </text>
<text fg={theme.text}> <text fg={theme.text}>
<b>Viewing subagent session</b> <b>Viewing subagent session</b>
</text> </text>
<text fg={theme.text}> <text fg={theme.text}>
<span style={{ fg: theme.textMuted }}> <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle")}</span> Next
{keybind.print("session_child_cycle")}
</span>{" "}
Next
</text> </text>
</box> </box>
</Show> </Show>
@@ -743,18 +717,12 @@ export function Session() {
paddingTop={1} paddingTop={1}
paddingBottom={1} paddingBottom={1}
paddingLeft={2} paddingLeft={2}
backgroundColor={ backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
hover() ? theme.backgroundElement : theme.backgroundPanel
}
> >
<text fg={theme.textMuted}>{revert()!.reverted.length} message reverted</text>
<text fg={theme.textMuted}> <text fg={theme.textMuted}>
{revert()!.reverted.length} message reverted <span style={{ fg: theme.text }}>{keybind.print("messages_redo")}</span> or /redo to
</text> restore
<text fg={theme.textMuted}>
<span style={{ fg: theme.text }}>
{keybind.print("messages_redo")}
</span>{" "}
or /redo to restore
</text> </text>
<Show when={revert()!.diffFiles?.length}> <Show when={revert()!.diffFiles?.length}>
<box marginTop={1}> <box marginTop={1}>
@@ -763,16 +731,10 @@ export function Session() {
<text> <text>
{file.filename} {file.filename}
<Show when={file.additions > 0}> <Show when={file.additions > 0}>
<span style={{ fg: theme.diffAdded }}> <span style={{ fg: theme.diffAdded }}> +{file.additions}</span>
{" "}
+{file.additions}
</span>
</Show> </Show>
<Show when={file.deletions > 0}> <Show when={file.deletions > 0}>
<span style={{ fg: theme.diffRemoved }}> <span style={{ fg: theme.diffRemoved }}> -{file.deletions}</span>
{" "}
-{file.deletions}
</span>
</Show> </Show>
</text> </text>
)} )}
@@ -792,9 +754,7 @@ export function Session() {
index={index()} index={index()}
onMouseUp={() => { onMouseUp={() => {
if (renderer.getSelection()?.getSelectedText()) return if (renderer.getSelection()?.getSelectedText()) return
dialog.replace(() => ( dialog.replace(() => <DialogMessage messageID={message.id} sessionID={route.sessionID} />)
<DialogMessage messageID={message.id} sessionID={route.sessionID} />
))
}} }}
message={message as UserMessage} message={message as UserMessage}
parts={sync.data.part[message.id] ?? []} parts={sync.data.part[message.id] ?? []}
@@ -850,9 +810,7 @@ function UserMessage(props: {
index: number index: number
pending?: string pending?: string
}) { }) {
const text = createMemo( const text = createMemo(() => props.parts.flatMap((x) => (x.type === "text" && !x.synthetic ? [x] : []))[0])
() => props.parts.flatMap((x) => (x.type === "text" && !x.synthetic ? [x] : []))[0],
)
const files = createMemo(() => props.parts.flatMap((x) => (x.type === "file" ? [x] : []))) const files = createMemo(() => props.parts.flatMap((x) => (x.type === "file" ? [x] : [])))
const sync = useSync() const sync = useSync()
const { theme } = useTheme() const { theme } = useTheme()
@@ -893,14 +851,8 @@ function UserMessage(props: {
}) })
return ( return (
<text fg={theme.text}> <text fg={theme.text}>
<span style={{ bg: bg(), fg: theme.background }}> <span style={{ bg: bg(), fg: theme.background }}> {MIME_BADGE[file.mime] ?? file.mime} </span>
{" "} <span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {file.filename} </span>
{MIME_BADGE[file.mime] ?? file.mime}{" "}
</span>
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}>
{" "}
{file.filename}{" "}
</span>
</text> </text>
) )
}} }}
@@ -911,16 +863,9 @@ function UserMessage(props: {
{sync.data.config.username ?? "You"}{" "} {sync.data.config.username ?? "You"}{" "}
<Show <Show
when={queued()} when={queued()}
fallback={ fallback={<span style={{ fg: theme.textMuted }}>({Locale.time(props.message.time.created)})</span>}
<span style={{ fg: theme.textMuted }}>
({Locale.time(props.message.time.created)})
</span>
}
> >
<span style={{ bg: theme.accent, fg: theme.backgroundPanel, bold: true }}> <span style={{ bg: theme.accent, fg: theme.backgroundPanel, bold: true }}> QUEUED </span>
{" "}
QUEUED{" "}
</span>
</Show> </Show>
</text> </text>
</box> </box>
@@ -960,8 +905,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
<Show <Show
when={ when={
!props.message.time.completed || !props.message.time.completed ||
(props.last && (props.last && props.parts.some((item) => item.type === "step-finish" && item.reason === "tool-calls"))
props.parts.some((item) => item.type === "step-finish" && item.reason === "tool-calls"))
} }
> >
<box <box
@@ -973,9 +917,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
customBorderChars={SplitBorder.customBorderChars} customBorderChars={SplitBorder.customBorderChars}
borderColor={theme.backgroundElement} borderColor={theme.backgroundElement}
> >
<text fg={local.agent.color(props.message.mode)}> <text fg={local.agent.color(props.message.mode)}>{Locale.titlecase(props.message.mode)}</text>
{Locale.titlecase(props.message.mode)}
</text>
<Shimmer text={`${props.message.modelID}`} color={theme.text} /> <Shimmer text={`${props.message.modelID}`} color={theme.text} />
</box> </box>
</Show> </Show>
@@ -987,9 +929,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
> >
<box paddingLeft={3}> <box paddingLeft={3}>
<text marginTop={1}> <text marginTop={1}>
<span style={{ fg: local.agent.color(props.message.mode) }}> <span style={{ fg: local.agent.color(props.message.mode) }}>{Locale.titlecase(props.message.mode)}</span>{" "}
{Locale.titlecase(props.message.mode)}
</span>{" "}
<span style={{ fg: theme.textMuted }}>{props.message.modelID}</span> <span style={{ fg: theme.textMuted }}>{props.message.modelID}</span>
</text> </text>
</box> </box>
@@ -1016,12 +956,7 @@ function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage }
customBorderChars={SplitBorder.customBorderChars} customBorderChars={SplitBorder.customBorderChars}
borderColor={theme.backgroundPanel} borderColor={theme.backgroundPanel}
> >
<box <box paddingTop={1} paddingBottom={1} paddingLeft={2} backgroundColor={theme.backgroundPanel}>
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
backgroundColor={theme.backgroundPanel}
>
<text fg={theme.text}>{props.part.text.trim()}</text> <text fg={theme.text}>{props.part.text.trim()}</text>
</box> </box>
</box> </box>
@@ -1261,16 +1196,10 @@ ToolRegistry.register<typeof WriteTool>({
</ToolTitle> </ToolTitle>
<box flexDirection="row"> <box flexDirection="row">
<box flexShrink={0}> <box flexShrink={0}>
<For each={numbers()}> <For each={numbers()}>{(value) => <text style={{ fg: theme.textMuted }}>{value}</text>}</For>
{(value) => <text style={{ fg: theme.textMuted }}>{value}</text>}
</For>
</box> </box>
<box paddingLeft={1} flexGrow={1}> <box paddingLeft={1} flexGrow={1}>
<code <code filetype={filetype(props.input.filePath!)} syntaxStyle={syntax()} content={code()} />
filetype={filetype(props.input.filePath!)}
syntaxStyle={syntax()}
content={code()}
/>
</box> </box>
</box> </box>
</> </>
@@ -1285,8 +1214,7 @@ ToolRegistry.register<typeof GlobTool>({
return ( return (
<> <>
<ToolTitle icon="✱" fallback="Finding files..." when={props.input.pattern}> <ToolTitle icon="✱" fallback="Finding files..." when={props.input.pattern}>
Glob "{props.input.pattern}"{" "} Glob "{props.input.pattern}" <Show when={props.input.path}>in {normalizePath(props.input.path)} </Show>
<Show when={props.input.path}>in {normalizePath(props.input.path)} </Show>
<Show when={props.metadata.count}>({props.metadata.count} matches)</Show> <Show when={props.metadata.count}>({props.metadata.count} matches)</Show>
</ToolTitle> </ToolTitle>
</> </>
@@ -1300,8 +1228,7 @@ ToolRegistry.register<typeof GrepTool>({
render(props) { render(props) {
return ( return (
<ToolTitle icon="✱" fallback="Searching content..." when={props.input.pattern}> <ToolTitle icon="✱" fallback="Searching content..." when={props.input.pattern}>
Grep "{props.input.pattern}"{" "} Grep "{props.input.pattern}" <Show when={props.input.path}>in {normalizePath(props.input.path)} </Show>
<Show when={props.input.path}>in {normalizePath(props.input.path)} </Show>
<Show when={props.metadata.matches}>({props.metadata.matches} matches)</Show> <Show when={props.metadata.matches}>({props.metadata.matches} matches)</Show>
</ToolTitle> </ToolTitle>
) )
@@ -1337,11 +1264,7 @@ ToolRegistry.register<typeof TaskTool>({
return ( return (
<> <>
<ToolTitle <ToolTitle icon="%" fallback="Delegating..." when={props.input.subagent_type ?? props.input.description}>
icon="%"
fallback="Delegating..."
when={props.input.subagent_type ?? props.input.description}
>
Task [{props.input.subagent_type ?? "unknown"}] {props.input.description} Task [{props.input.subagent_type ?? "unknown"}] {props.input.description}
</ToolTitle> </ToolTitle>
<Show when={props.metadata.summary?.length}> <Show when={props.metadata.summary?.length}>

View File

@@ -22,16 +22,10 @@ export function Sidebar(props: { sessionID: string }) {
}) })
const context = createMemo(() => { const context = createMemo(() => {
const last = messages().findLast( const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage
(x) => x.role === "assistant" && x.tokens.output > 0,
) as AssistantMessage
if (!last) return if (!last) return
const total = const total =
last.tokens.input + last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
last.tokens.output +
last.tokens.reasoning +
last.tokens.cache.read +
last.tokens.cache.write
const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID] const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID]
return { return {
tokens: total.toLocaleString(), tokens: total.toLocaleString(),
@@ -84,9 +78,7 @@ export function Sidebar(props: { sessionID: string }) {
<span style={{ fg: theme.textMuted }}> <span style={{ fg: theme.textMuted }}>
<Switch> <Switch>
<Match when={item.status === "connected"}>Connected</Match> <Match when={item.status === "connected"}>Connected</Match>
<Match when={item.status === "failed" && item}> <Match when={item.status === "failed" && item}>{(val) => <i>{val().error}</i>}</Match>
{(val) => <i>{val().error}</i>}
</Match>
<Match when={item.status === "disabled"}>Disabled in configuration</Match> <Match when={item.status === "disabled"}>Disabled in configuration</Match>
</Switch> </Switch>
</span> </span>
@@ -162,9 +154,7 @@ export function Sidebar(props: { sessionID: string }) {
</text> </text>
<For each={todo()}> <For each={todo()}>
{(todo) => ( {(todo) => (
<text <text style={{ fg: todo.status === "in_progress" ? theme.success : theme.textMuted }}>
style={{ fg: todo.status === "in_progress" ? theme.success : theme.textMuted }}
>
[{todo.status === "completed" ? "✓" : " "}] {todo.content} [{todo.status === "completed" ? "✓" : " "}] {todo.content}
</text> </text>
)} )}

View File

@@ -41,12 +41,7 @@ export const TuiSpawnCommand = cmd({
) )
cwd = new URL("../../../../", import.meta.url).pathname cwd = new URL("../../../../", import.meta.url).pathname
} else cmd.push(process.execPath) } else cmd.push(process.execPath)
cmd.push( cmd.push("attach", server.url.toString(), "--dir", args.project ? path.resolve(args.project) : process.cwd())
"attach",
server.url.toString(),
"--dir",
args.project ? path.resolve(args.project) : process.cwd(),
)
const proc = Bun.spawn({ const proc = Bun.spawn({
cmd, cmd,
cwd, cwd,

View File

@@ -99,9 +99,7 @@ export const TuiThreadCommand = cmd({
const worker = new Worker(workerPath, { const worker = new Worker(workerPath, {
env: Object.fromEntries( env: Object.fromEntries(
Object.entries(process.env).filter( Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
(entry): entry is [string, string] => entry[1] !== undefined,
),
), ),
}) })
worker.onerror = console.error worker.onerror = console.error

View File

@@ -53,9 +53,7 @@ export function DialogConfirm(props: DialogConfirmProps) {
dialog.clear() dialog.clear()
}} }}
> >
<text fg={key === store.active ? theme.background : theme.textMuted}> <text fg={key === store.active ? theme.background : theme.textMuted}>{Locale.titlecase(key)}</text>
{Locale.titlecase(key)}
</text>
</box> </box>
)} )}
</For> </For>

View File

@@ -20,17 +20,10 @@ export function DialogHelp() {
<text fg={theme.textMuted}>esc/enter</text> <text fg={theme.textMuted}>esc/enter</text>
</box> </box>
<box paddingBottom={1}> <box paddingBottom={1}>
<text fg={theme.textMuted}> <text fg={theme.textMuted}>Press Ctrl+P to see all available actions and commands in any context.</text>
Press Ctrl+P to see all available actions and commands in any context.
</text>
</box> </box>
<box flexDirection="row" justifyContent="flex-end" paddingBottom={1}> <box flexDirection="row" justifyContent="flex-end" paddingBottom={1}>
<box <box paddingLeft={3} paddingRight={3} backgroundColor={theme.primary} onMouseUp={() => dialog.clear()}>
paddingLeft={3}
paddingRight={3}
backgroundColor={theme.primary}
onMouseUp={() => dialog.clear()}
>
<text fg={theme.background}>ok</text> <text fg={theme.background}>ok</text>
</box> </box>
</box> </box>

View File

@@ -57,8 +57,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
const result = pipe( const result = pipe(
props.options, props.options,
filter((x) => x.disabled !== true), filter((x) => x.disabled !== true),
(x) => (x) => (!needle ? x : fuzzysort.go(needle, x, { keys: ["title", "category"] }).map((x) => x.obj)),
!needle ? x : fuzzysort.go(needle, x, { keys: ["title", "category"] }).map((x) => x.obj),
) )
return result return result
}) })
@@ -214,15 +213,11 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
props.onSelect?.(option) props.onSelect?.(option)
}} }}
onMouseOver={() => { onMouseOver={() => {
const index = filtered().findIndex((x) => const index = filtered().findIndex((x) => isDeepEqual(x.value, option.value))
isDeepEqual(x.value, option.value),
)
if (index === -1) return if (index === -1) return
moveTo(index) moveTo(index)
}} }}
backgroundColor={ backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)}
active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)
}
paddingLeft={1} paddingLeft={1}
paddingRight={1} paddingRight={1}
gap={1} gap={1}
@@ -230,9 +225,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
<Option <Option
title={option.title} title={option.title}
footer={option.footer} footer={option.footer}
description={ description={option.description !== category ? option.description : undefined}
option.description !== category ? option.description : undefined
}
active={active()} active={active()}
current={isDeepEqual(option.value, props.current)} current={isDeepEqual(option.value, props.current)}
/> />
@@ -248,9 +241,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
<For each={props.keybind ?? []}> <For each={props.keybind ?? []}>
{(item) => ( {(item) => (
<text> <text>
<span style={{ fg: theme.text, attributes: TextAttributes.BOLD }}> <span style={{ fg: theme.text, attributes: TextAttributes.BOLD }}>{Keybind.toString(item.keybind)}</span>
{Keybind.toString(item.keybind)}
</span>
<span style={{ fg: theme.textMuted }}> {item.title}</span> <span style={{ fg: theme.textMuted }}> {item.title}</span>
</text> </text>
)} )}
@@ -284,10 +275,7 @@ function Option(props: {
wrapMode="none" wrapMode="none"
> >
{Locale.truncate(props.title, 62)} {Locale.truncate(props.title, 62)}
<span style={{ fg: props.active ? theme.background : theme.textMuted }}> <span style={{ fg: props.active ? theme.background : theme.textMuted }}> {props.description}</span>
{" "}
{props.description}
</span>
</text> </text>
<Show when={props.footer}> <Show when={props.footer}>
<box flexShrink={0}> <box flexShrink={0}>

View File

@@ -5,10 +5,7 @@ import { join } from "node:path"
import { CliRenderer } from "@opentui/core" import { CliRenderer } from "@opentui/core"
export namespace Editor { export namespace Editor {
export async function open(opts: { export async function open(opts: { value: string; renderer: CliRenderer }): Promise<string | undefined> {
value: string
renderer: CliRenderer
}): Promise<string | undefined> {
const editor = process.env["EDITOR"] const editor = process.env["EDITOR"]
if (!editor) return if (!editor) return

View File

@@ -27,9 +27,7 @@ export const UpgradeCommand = {
const detectedMethod = await Installation.method() const detectedMethod = await Installation.method()
const method = (args.method as Installation.Method) ?? detectedMethod const method = (args.method as Installation.Method) ?? detectedMethod
if (method === "unknown") { if (method === "unknown") {
prompts.log.error( prompts.log.error(`opencode is installed to ${process.execPath} and may be managed by a package manager`)
`opencode is installed to ${process.execPath} and may be managed by a package manager`,
)
const install = await prompts.select({ const install = await prompts.select({
message: "Install anyways?", message: "Install anyways?",
options: [ options: [

View File

@@ -56,11 +56,7 @@ export const WebCommand = cmd({
if (hostname === "0.0.0.0") { if (hostname === "0.0.0.0") {
// Show localhost for local access // Show localhost for local access
const localhostUrl = `http://localhost:${server.port}` const localhostUrl = `http://localhost:${server.port}`
UI.println( UI.println(UI.Style.TEXT_INFO_BOLD + " Local access: ", UI.Style.TEXT_NORMAL, localhostUrl)
UI.Style.TEXT_INFO_BOLD + " Local access: ",
UI.Style.TEXT_NORMAL,
localhostUrl,
)
// Show network IPs for remote access // Show network IPs for remote access
const networkIPs = getNetworkIPs() const networkIPs = getNetworkIPs()

View File

@@ -8,8 +8,7 @@ export function FormatError(input: unknown) {
return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.` return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.`
if (Config.JsonError.isInstance(input)) { if (Config.JsonError.isInstance(input)) {
return ( return (
`Config file at ${input.data.path} is not valid JSON(C)` + `Config file at ${input.data.path} is not valid JSON(C)` + (input.data.message ? `: ${input.data.message}` : "")
(input.data.message ? `: ${input.data.message}` : "")
) )
} }
if (Config.ConfigDirectoryTypoError.isInstance(input)) { if (Config.ConfigDirectoryTypoError.isInstance(input)) {
@@ -20,10 +19,8 @@ export function FormatError(input: unknown) {
} }
if (Config.InvalidError.isInstance(input)) if (Config.InvalidError.isInstance(input))
return [ return [
`Config file at ${input.data.path} is invalid` + `Config file at ${input.data.path} is invalid` + (input.data.message ? `: ${input.data.message}` : ""),
(input.data.message ? `: ${input.data.message}` : ""), ...(input.data.issues?.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) ?? []),
...(input.data.issues?.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) ??
[]),
].join("\n") ].join("\n")
if (UI.CancelledError.isInstance(input)) return "" if (UI.CancelledError.isInstance(input)) return ""

View File

@@ -11,11 +11,7 @@ import { lazy } from "../util/lazy"
import { NamedError } from "../util/error" import { NamedError } from "../util/error"
import { Flag } from "../flag/flag" import { Flag } from "../flag/flag"
import { Auth } from "../auth" import { Auth } from "../auth"
import { import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser"
type ParseError as JsoncParseError,
parse as parseJsonc,
printParseErrorCode,
} from "jsonc-parser"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
import { LSPServer } from "../lsp/server" import { LSPServer } from "../lsp/server"
import { BunProc } from "@/bun" import { BunProc } from "@/bun"
@@ -50,10 +46,7 @@ export namespace Config {
if (value.type === "wellknown") { if (value.type === "wellknown") {
process.env[value.key] = value.token process.env[value.key] = value.token
const wellknown = (await fetch(`${key}/.well-known/opencode`).then((x) => x.json())) as any const wellknown = (await fetch(`${key}/.well-known/opencode`).then((x) => x.json())) as any
result = mergeDeep( result = mergeDeep(result, await load(JSON.stringify(wellknown.config ?? {}), process.cwd()))
result,
await load(JSON.stringify(wellknown.config ?? {}), process.cwd()),
)
} }
} }
@@ -159,18 +152,10 @@ export namespace Config {
const gitignore = path.join(dir, ".gitignore") const gitignore = path.join(dir, ".gitignore")
const hasGitIgnore = await Bun.file(gitignore).exists() const hasGitIgnore = await Bun.file(gitignore).exists()
if (!hasGitIgnore) if (!hasGitIgnore) await Bun.write(gitignore, ["node_modules", "package.json", "bun.lock", ".gitignore"].join("\n"))
await Bun.write(
gitignore,
["node_modules", "package.json", "bun.lock", ".gitignore"].join("\n"),
)
await BunProc.run( await BunProc.run(
[ ["add", "@opencode-ai/plugin@" + (Installation.isLocal() ? "latest" : Installation.VERSION), "--exact"],
"add",
"@opencode-ai/plugin@" + (Installation.isLocal() ? "latest" : Installation.VERSION),
"--exact",
],
{ {
cwd: dir, cwd: dir,
}, },
@@ -330,10 +315,7 @@ export namespace Config {
type: z.literal("remote").describe("Type of MCP server connection"), type: z.literal("remote").describe("Type of MCP server connection"),
url: z.string().describe("URL of the remote MCP server"), url: z.string().describe("URL of the remote MCP server"),
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"), enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
headers: z headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"),
.record(z.string(), z.string())
.optional()
.describe("Headers to send with the request"),
timeout: z timeout: z
.number() .number()
.int() .int()
@@ -389,70 +371,30 @@ export namespace Config {
export const Keybinds = z export const Keybinds = z
.object({ .object({
leader: z leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
.string() app_exit: z.string().optional().default("ctrl+c,ctrl+d,<leader>q").describe("Exit the application"),
.optional()
.default("ctrl+x")
.describe("Leader key for keybind combinations"),
app_exit: z
.string()
.optional()
.default("ctrl+c,ctrl+d,<leader>q")
.describe("Exit the application"),
editor_open: z.string().optional().default("<leader>e").describe("Open external editor"), editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
theme_list: z.string().optional().default("<leader>t").describe("List available themes"), theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
sidebar_toggle: z.string().optional().default("<leader>b").describe("Toggle sidebar"), sidebar_toggle: z.string().optional().default("<leader>b").describe("Toggle sidebar"),
status_view: z.string().optional().default("<leader>s").describe("View status"), status_view: z.string().optional().default("<leader>s").describe("View status"),
session_export: z session_export: z.string().optional().default("<leader>x").describe("Export session to editor"),
.string()
.optional()
.default("<leader>x")
.describe("Export session to editor"),
session_new: z.string().optional().default("<leader>n").describe("Create a new session"), session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
session_list: z.string().optional().default("<leader>l").describe("List all sessions"), session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
session_timeline: z session_timeline: z.string().optional().default("<leader>g").describe("Show session timeline"),
.string()
.optional()
.default("<leader>g")
.describe("Show session timeline"),
session_share: z.string().optional().default("none").describe("Share current session"), session_share: z.string().optional().default("none").describe("Share current session"),
session_unshare: z.string().optional().default("none").describe("Unshare current session"), session_unshare: z.string().optional().default("none").describe("Unshare current session"),
session_interrupt: z session_interrupt: z.string().optional().default("escape").describe("Interrupt current session"),
.string()
.optional()
.default("escape")
.describe("Interrupt current session"),
session_compact: z.string().optional().default("<leader>c").describe("Compact the session"), session_compact: z.string().optional().default("<leader>c").describe("Compact the session"),
messages_page_up: z messages_page_up: z.string().optional().default("pageup").describe("Scroll messages up by one page"),
.string() messages_page_down: z.string().optional().default("pagedown").describe("Scroll messages down by one page"),
.optional() messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"),
.default("pageup")
.describe("Scroll messages up by one page"),
messages_page_down: z
.string()
.optional()
.default("pagedown")
.describe("Scroll messages down by one page"),
messages_half_page_up: z
.string()
.optional()
.default("ctrl+alt+u")
.describe("Scroll messages up by half page"),
messages_half_page_down: z messages_half_page_down: z
.string() .string()
.optional() .optional()
.default("ctrl+alt+d") .default("ctrl+alt+d")
.describe("Scroll messages down by half page"), .describe("Scroll messages down by half page"),
messages_first: z messages_first: z.string().optional().default("ctrl+g,home").describe("Navigate to first message"),
.string() messages_last: z.string().optional().default("ctrl+alt+g,end").describe("Navigate to last message"),
.optional()
.default("ctrl+g,home")
.describe("Navigate to first message"),
messages_last: z
.string()
.optional()
.default("ctrl+alt+g,end")
.describe("Navigate to last message"),
messages_copy: z.string().optional().default("<leader>y").describe("Copy message"), messages_copy: z.string().optional().default("<leader>y").describe("Copy message"),
messages_undo: z.string().optional().default("<leader>u").describe("Undo message"), messages_undo: z.string().optional().default("<leader>u").describe("Undo message"),
messages_redo: z.string().optional().default("<leader>r").describe("Redo message"), messages_redo: z.string().optional().default("<leader>r").describe("Redo message"),
@@ -463,11 +405,7 @@ export namespace Config {
.describe("Toggle code block concealment in messages"), .describe("Toggle code block concealment in messages"),
model_list: z.string().optional().default("<leader>m").describe("List available models"), model_list: z.string().optional().default("<leader>m").describe("List available models"),
model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"), model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"),
model_cycle_recent_reverse: z model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recently used model"),
.string()
.optional()
.default("shift+f2")
.describe("Previous recently used model"),
command_list: z.string().optional().default("ctrl+p").describe("List available commands"), command_list: z.string().optional().default("ctrl+p").describe("List available commands"),
agent_list: z.string().optional().default("<leader>a").describe("List agents"), agent_list: z.string().optional().default("<leader>a").describe("List agents"),
agent_cycle: z.string().optional().default("tab").describe("Next agent"), agent_cycle: z.string().optional().default("tab").describe("Next agent"),
@@ -476,23 +414,11 @@ export namespace Config {
input_forward_delete: z.string().optional().default("ctrl+d").describe("Forward delete"), input_forward_delete: z.string().optional().default("ctrl+d").describe("Forward delete"),
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"), input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
input_submit: z.string().optional().default("return").describe("Submit input"), input_submit: z.string().optional().default("return").describe("Submit input"),
input_newline: z input_newline: z.string().optional().default("shift+return,ctrl+j").describe("Insert newline in input"),
.string()
.optional()
.default("shift+return,ctrl+j")
.describe("Insert newline in input"),
history_previous: z.string().optional().default("up").describe("Previous history item"), history_previous: z.string().optional().default("up").describe("Previous history item"),
history_next: z.string().optional().default("down").describe("Next history item"), history_next: z.string().optional().default("down").describe("Next history item"),
session_child_cycle: z session_child_cycle: z.string().optional().default("ctrl+right").describe("Next child session"),
.string() session_child_cycle_reverse: z.string().optional().default("ctrl+left").describe("Previous child session"),
.optional()
.default("ctrl+right")
.describe("Next child session"),
session_child_cycle_reverse: z
.string()
.optional()
.default("ctrl+left")
.describe("Previous child session"),
}) })
.strict() .strict()
.meta({ .meta({
@@ -534,23 +460,13 @@ export namespace Config {
autoshare: z autoshare: z
.boolean() .boolean()
.optional() .optional()
.describe( .describe("@deprecated Use 'share' field instead. Share newly created sessions automatically"),
"@deprecated Use 'share' field instead. Share newly created sessions automatically",
),
autoupdate: z.boolean().optional().describe("Automatically update to the latest version"), autoupdate: z.boolean().optional().describe("Automatically update to the latest version"),
disabled_providers: z disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"),
.array(z.string()) model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
.optional()
.describe("Disable providers that are loaded automatically"),
model: z
.string()
.describe("Model to use in the format of provider/model, eg anthropic/claude-2")
.optional(),
small_model: z small_model: z
.string() .string()
.describe( .describe("Small model to use for tasks like title generation in the format of provider/model")
"Small model to use for tasks like title generation in the format of provider/model",
)
.optional(), .optional(),
username: z username: z
.string() .string()
@@ -583,10 +499,7 @@ export namespace Config {
.object({ .object({
apiKey: z.string().optional(), apiKey: z.string().optional(),
baseURL: z.string().optional(), baseURL: z.string().optional(),
enterpriseUrl: z enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"),
.string()
.optional()
.describe("GitHub Enterprise URL for copilot authentication"),
timeout: z timeout: z
.union([ .union([
z z
@@ -610,10 +523,7 @@ export namespace Config {
) )
.optional() .optional()
.describe("Custom provider configurations and model overrides"), .describe("Custom provider configurations and model overrides"),
mcp: z mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
.record(z.string(), Mcp)
.optional()
.describe("MCP (Model Context Protocol) server configurations"),
formatter: z formatter: z
.record( .record(
z.string(), z.string(),
@@ -657,10 +567,7 @@ export namespace Config {
error: "For custom LSP servers, 'extensions' array is required.", error: "For custom LSP servers, 'extensions' array is required.",
}, },
), ),
instructions: z instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
.array(z.string())
.optional()
.describe("Additional instruction files or patterns to include"),
layout: Layout.optional().describe("@deprecated Always uses stretch layout."), layout: Layout.optional().describe("@deprecated Always uses stretch layout."),
permission: z permission: z
.object({ .object({
@@ -694,10 +601,7 @@ export namespace Config {
.optional(), .optional(),
}) })
.optional(), .optional(),
chatMaxRetries: z chatMaxRetries: z.number().optional().describe("Number of retries for chat completions on failure"),
.number()
.optional()
.describe("Number of retries for chat completions on failure"),
disable_paste_summary: z.boolean().optional(), disable_paste_summary: z.boolean().optional(),
}) })
.optional(), .optional(),
@@ -727,10 +631,7 @@ export namespace Config {
if (provider && model) result.model = `${provider}/${model}` if (provider && model) result.model = `${provider}/${model}`
result["$schema"] = "https://opencode.ai/config.json" result["$schema"] = "https://opencode.ai/config.json"
result = mergeDeep(result, rest) result = mergeDeep(result, rest)
await Bun.write( await Bun.write(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2))
path.join(Global.Path.config, "config.json"),
JSON.stringify(result, null, 2),
)
await fs.unlink(path.join(Global.Path.config, "config")) await fs.unlink(path.join(Global.Path.config, "config"))
}) })
.catch(() => {}) .catch(() => {})
@@ -769,9 +670,7 @@ export namespace Config {
if (filePath.startsWith("~/")) { if (filePath.startsWith("~/")) {
filePath = path.join(os.homedir(), filePath.slice(2)) filePath = path.join(os.homedir(), filePath.slice(2))
} }
const resolvedPath = path.isAbsolute(filePath) const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
? filePath
: path.resolve(configDir, filePath)
const fileContent = ( const fileContent = (
await Bun.file(resolvedPath) await Bun.file(resolvedPath)
.text() .text()

View File

@@ -81,9 +81,7 @@ export namespace Fzf {
}) })
} }
if (config.extension === "zip") { if (config.extension === "zip") {
const zipFileReader = new ZipReader( const zipFileReader = new ZipReader(new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()])))
new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()])),
)
const entries = await zipFileReader.getEntries() const entries = await zipFileReader.getEntries()
let fzfEntry: any let fzfEntry: any
for (const entry of entries) { for (const entry of entries) {

View File

@@ -165,11 +165,7 @@ export namespace File {
const project = Instance.project const project = Instance.project
if (project.vcs !== "git") return [] if (project.vcs !== "git") return []
const diffOutput = await $`git diff --numstat HEAD` const diffOutput = await $`git diff --numstat HEAD`.cwd(Instance.directory).quiet().nothrow().text()
.cwd(Instance.directory)
.quiet()
.nothrow()
.text()
const changedFiles: Info[] = [] const changedFiles: Info[] = []
@@ -261,14 +257,9 @@ export namespace File {
if (project.vcs === "git") { if (project.vcs === "git") {
let diff = await $`git diff ${file}`.cwd(Instance.directory).quiet().nothrow().text() let diff = await $`git diff ${file}`.cwd(Instance.directory).quiet().nothrow().text()
if (!diff.trim()) if (!diff.trim()) diff = await $`git diff --staged ${file}`.cwd(Instance.directory).quiet().nothrow().text()
diff = await $`git diff --staged ${file}`.cwd(Instance.directory).quiet().nothrow().text()
if (diff.trim()) { if (diff.trim()) {
const original = await $`git show HEAD:${file}` const original = await $`git show HEAD:${file}`.cwd(Instance.directory).quiet().nothrow().text()
.cwd(Instance.directory)
.quiet()
.nothrow()
.text()
const patch = structuredPatch(file, file, original, content, "old", "new", { const patch = structuredPatch(file, file, original, content, "old", "new", {
context: Infinity, context: Infinity,
ignoreWhitespace: true, ignoreWhitespace: true,
@@ -321,9 +312,7 @@ export namespace File {
const limit = input.limit ?? 100 const limit = input.limit ?? 100
const result = await state().then((x) => x.files()) const result = await state().then((x) => x.files())
if (!input.query) if (!input.query)
return input.dirs !== false return input.dirs !== false ? result.dirs.toSorted().slice(0, limit) : result.files.slice(0, limit)
? result.dirs.toSorted().slice(0, limit)
: result.files.slice(0, limit)
const items = input.dirs !== false ? [...result.files, ...result.dirs] : result.files const items = input.dirs !== false ? [...result.files, ...result.dirs] : result.files
const sorted = fuzzysort.go(input.query, items, { limit: limit }).map((r) => r.target) const sorted = fuzzysort.go(input.query, items, { limit: limit }).map((r) => r.target)
log.info("search", { query: input.query, results: sorted.length }) log.info("search", { query: input.query, results: sorted.length })

View File

@@ -161,9 +161,7 @@ export namespace Ripgrep {
} }
if (config.extension === "zip") { if (config.extension === "zip") {
if (config.extension === "zip") { if (config.extension === "zip") {
const zipFileReader = new ZipReader( const zipFileReader = new ZipReader(new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()])))
new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()])),
)
const entries = await zipFileReader.getEntries() const entries = await zipFileReader.getEntries()
let rgEntry: any let rgEntry: any
for (const entry of entries) { for (const entry of entries) {
@@ -356,12 +354,7 @@ export namespace Ripgrep {
return lines.join("\n") return lines.join("\n")
} }
export async function search(input: { export async function search(input: { cwd: string; pattern: string; glob?: string[]; limit?: number }) {
cwd: string
pattern: string
glob?: string[]
limit?: number
}) {
const args = [`${await filepath()}`, "--json", "--hidden", "--glob='!.git/*'"] const args = [`${await filepath()}`, "--json", "--hidden", "--glob='!.git/*'"]
if (input.glob) { if (input.glob) {

View File

@@ -27,10 +27,7 @@ export namespace FileTime {
export async function assert(sessionID: string, filepath: string) { export async function assert(sessionID: string, filepath: string) {
const time = get(sessionID, filepath) const time = get(sessionID, filepath)
if (!time) if (!time) throw new Error(`You must read the file ${filepath} before overwriting it. Use the Read tool first`)
throw new Error(
`You must read the file ${filepath} before overwriting it. Use the Read tool first`,
)
const stats = await Bun.file(filepath).stat() const stats = await Bun.file(filepath).stat()
if (stats.mtime.getTime() > time.getTime()) { if (stats.mtime.getTime() > time.getTime()) {
throw new Error( throw new Error(

View File

@@ -51,10 +51,8 @@ export namespace FileWatcher {
for (const evt of evts) { for (const evt of evts) {
log.info("event", evt) log.info("event", evt)
if (evt.type === "create") Bus.publish(Event.Updated, { file: evt.path, event: "add" }) if (evt.type === "create") Bus.publish(Event.Updated, { file: evt.path, event: "add" })
if (evt.type === "update") if (evt.type === "update") Bus.publish(Event.Updated, { file: evt.path, event: "change" })
Bus.publish(Event.Updated, { file: evt.path, event: "change" }) if (evt.type === "delete") Bus.publish(Event.Updated, { file: evt.path, event: "unlink" })
if (evt.type === "delete")
Bus.publish(Event.Updated, { file: evt.path, event: "unlink" })
} }
}, },
{ {

View File

@@ -132,21 +132,7 @@ export const zig: Info = {
export const clang: Info = { export const clang: Info = {
name: "clang-format", name: "clang-format",
command: ["clang-format", "-i", "$FILE"], command: ["clang-format", "-i", "$FILE"],
extensions: [ extensions: [".c", ".cc", ".cpp", ".cxx", ".c++", ".h", ".hh", ".hpp", ".hxx", ".h++", ".ino", ".C", ".H"],
".c",
".cc",
".cpp",
".cxx",
".c++",
".h",
".hh",
".hpp",
".hxx",
".h++",
".ino",
".C",
".H",
],
async enabled() { async enabled() {
const items = await Filesystem.findUp(".clang-format", Instance.directory, Instance.worktree) const items = await Filesystem.findUp(".clang-format", Instance.directory, Instance.worktree)
return items.length > 0 return items.length > 0

View File

@@ -49,11 +49,7 @@ export namespace Identifier {
return result return result
} }
export function create( export function create(prefix: keyof typeof prefixes, descending: boolean, timestamp?: number): string {
prefix: keyof typeof prefixes,
descending: boolean,
timestamp?: number,
): string {
const currentTimestamp = timestamp ?? Date.now() const currentTimestamp = timestamp ?? Date.now()
if (currentTimestamp !== lastTimestamp) { if (currentTimestamp !== lastTimestamp) {

View File

@@ -44,10 +44,7 @@ export namespace Ide {
} }
export function alreadyInstalled() { export function alreadyInstalled() {
return ( return process.env["OPENCODE_CALLER"] === "vscode" || process.env["OPENCODE_CALLER"] === "vscode-insiders"
process.env["OPENCODE_CALLER"] === "vscode" ||
process.env["OPENCODE_CALLER"] === "vscode-insiders"
)
} }
export async function install(ide: (typeof SUPPORTED_IDES)[number]["name"]) { export async function install(ide: (typeof SUPPORTED_IDES)[number]["name"]) {

View File

@@ -1,9 +1,5 @@
import path from "path" import path from "path"
import { import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node"
createMessageConnection,
StreamMessageReader,
StreamMessageWriter,
} from "vscode-jsonrpc/node"
import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types" import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
import { Log } from "../util/log" import { Log } from "../util/log"
import { LANGUAGE_EXTENSIONS } from "./language" import { LANGUAGE_EXTENSIONS } from "./language"
@@ -38,11 +34,7 @@ export namespace LSPClient {
), ),
} }
export async function create(input: { export async function create(input: { serverID: string; server: LSPServer.Handle; root: string }) {
serverID: string
server: LSPServer.Handle
root: string
}) {
const l = log.clone().tag("serverID", input.serverID) const l = log.clone().tag("serverID", input.serverID)
l.info("starting client") l.info("starting client")
@@ -137,9 +129,7 @@ export namespace LSPClient {
}, },
notify: { notify: {
async open(input: { path: string }) { async open(input: { path: string }) {
input.path = path.isAbsolute(input.path) input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path)
? input.path
: path.resolve(Instance.directory, input.path)
const file = Bun.file(input.path) const file = Bun.file(input.path)
const text = await file.text() const text = await file.text()
const extension = path.extname(input.path) const extension = path.extname(input.path)
@@ -181,18 +171,13 @@ export namespace LSPClient {
return diagnostics return diagnostics
}, },
async waitForDiagnostics(input: { path: string }) { async waitForDiagnostics(input: { path: string }) {
input.path = path.isAbsolute(input.path) input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path)
? input.path
: path.resolve(Instance.directory, input.path)
log.info("waiting for diagnostics", input) log.info("waiting for diagnostics", input)
let unsub: () => void let unsub: () => void
return await withTimeout( return await withTimeout(
new Promise<void>((resolve) => { new Promise<void>((resolve) => {
unsub = Bus.subscribe(Event.Diagnostics, (event) => { unsub = Bus.subscribe(Event.Diagnostics, (event) => {
if ( if (event.properties.path === input.path && event.properties.serverID === result.serverID) {
event.properties.path === input.path &&
event.properties.serverID === result.serverID
) {
log.info("got diagnostics", input) log.info("got diagnostics", input)
unsub?.() unsub?.()
resolve() resolve()

View File

@@ -197,9 +197,7 @@ export namespace LSP {
await run(async (client) => { await run(async (client) => {
if (!clients.includes(client)) return if (!clients.includes(client)) return
const wait = waitForDiagnostics const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
? client.waitForDiagnostics({ path: input })
: Promise.resolve()
await client.notify.open({ path: input }) await client.notify.open({ path: input })
return wait return wait
}).catch((err) => { }).catch((err) => {

View File

@@ -88,9 +88,7 @@ export namespace LSPServer {
), ),
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"], extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
async spawn(root) { async spawn(root) {
const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch( const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(() => {})
() => {},
)
if (!tsserver) return if (!tsserver) return
const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], { const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], {
cwd: root, cwd: root,
@@ -113,13 +111,7 @@ export namespace LSPServer {
export const Vue: Info = { export const Vue: Info = {
id: "vue", id: "vue",
extensions: [".vue"], extensions: [".vue"],
root: NearestRoot([ root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
"package-lock.json",
"bun.lockb",
"bun.lock",
"pnpm-lock.yaml",
"yarn.lock",
]),
async spawn(root) { async spawn(root) {
let binary = Bun.which("vue-language-server") let binary = Bun.which("vue-language-server")
const args: string[] = [] const args: string[] = []
@@ -167,31 +159,17 @@ export namespace LSPServer {
export const ESLint: Info = { export const ESLint: Info = {
id: "eslint", id: "eslint",
root: NearestRoot([ root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
"package-lock.json",
"bun.lockb",
"bun.lock",
"pnpm-lock.yaml",
"yarn.lock",
]),
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue"], extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue"],
async spawn(root) { async spawn(root) {
const eslint = await Bun.resolve("eslint", Instance.directory).catch(() => {}) const eslint = await Bun.resolve("eslint", Instance.directory).catch(() => {})
if (!eslint) return if (!eslint) return
log.info("spawning eslint server") log.info("spawning eslint server")
const serverPath = path.join( const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js")
Global.Path.bin,
"vscode-eslint",
"server",
"out",
"eslintServer.js",
)
if (!(await Bun.file(serverPath).exists())) { if (!(await Bun.file(serverPath).exists())) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("downloading and building VS Code ESLint server") log.info("downloading and building VS Code ESLint server")
const response = await fetch( const response = await fetch("https://github.com/microsoft/vscode-eslint/archive/refs/heads/main.zip")
"https://github.com/microsoft/vscode-eslint/archive/refs/heads/main.zip",
)
if (!response.ok) return if (!response.ok) return
const zipPath = path.join(Global.Path.bin, "vscode-eslint.zip") const zipPath = path.join(Global.Path.bin, "vscode-eslint.zip")
@@ -316,25 +294,12 @@ export namespace LSPServer {
export const Pyright: Info = { export const Pyright: Info = {
id: "pyright", id: "pyright",
extensions: [".py", ".pyi"], extensions: [".py", ".pyi"],
root: NearestRoot([ root: NearestRoot(["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]),
"pyproject.toml",
"setup.py",
"setup.cfg",
"requirements.txt",
"Pipfile",
"pyrightconfig.json",
]),
async spawn(root) { async spawn(root) {
let binary = Bun.which("pyright-langserver") let binary = Bun.which("pyright-langserver")
const args = [] const args = []
if (!binary) { if (!binary) {
const js = path.join( const js = path.join(Global.Path.bin, "node_modules", "pyright", "dist", "pyright-langserver.js")
Global.Path.bin,
"node_modules",
"pyright",
"dist",
"pyright-langserver.js",
)
if (!(await Bun.file(js).exists())) { if (!(await Bun.file(js).exists())) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
await Bun.spawn([BunProc.which(), "install", "pyright"], { await Bun.spawn([BunProc.which(), "install", "pyright"], {
@@ -352,11 +317,9 @@ export namespace LSPServer {
const initialization: Record<string, string> = {} const initialization: Record<string, string> = {}
const potentialVenvPaths = [ const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")].filter(
process.env["VIRTUAL_ENV"], (p): p is string => p !== undefined,
path.join(root, ".venv"), )
path.join(root, "venv"),
].filter((p): p is string => p !== undefined)
for (const venvPath of potentialVenvPaths) { for (const venvPath of potentialVenvPaths) {
const isWindows = process.platform === "win32" const isWindows = process.platform === "win32"
const potentialPythonPath = isWindows const potentialPythonPath = isWindows
@@ -407,9 +370,7 @@ export namespace LSPServer {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("downloading elixir-ls from GitHub releases") log.info("downloading elixir-ls from GitHub releases")
const response = await fetch( const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip")
"https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip",
)
if (!response.ok) return if (!response.ok) return
const zipPath = path.join(Global.Path.bin, "elixir-ls.zip") const zipPath = path.join(Global.Path.bin, "elixir-ls.zip")
await Bun.file(zipPath).write(response) await Bun.file(zipPath).write(response)
@@ -459,9 +420,7 @@ export namespace LSPServer {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("downloading zls from GitHub releases") log.info("downloading zls from GitHub releases")
const releaseResponse = await fetch( const releaseResponse = await fetch("https://api.github.com/repos/zigtools/zls/releases/latest")
"https://api.github.com/repos/zigtools/zls/releases/latest",
)
if (!releaseResponse.ok) { if (!releaseResponse.ok) {
log.error("Failed to fetch zls release info") log.error("Failed to fetch zls release info")
return return
@@ -636,13 +595,7 @@ export namespace LSPServer {
export const Clangd: Info = { export const Clangd: Info = {
id: "clangd", id: "clangd",
root: NearestRoot([ root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd", "CMakeLists.txt", "Makefile"]),
"compile_commands.json",
"compile_flags.txt",
".clangd",
"CMakeLists.txt",
"Makefile",
]),
extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"], extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
async spawn(root) { async spawn(root) {
let bin = Bun.which("clangd", { let bin = Bun.which("clangd", {
@@ -652,9 +605,7 @@ export namespace LSPServer {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("downloading clangd from GitHub releases") log.info("downloading clangd from GitHub releases")
const releaseResponse = await fetch( const releaseResponse = await fetch("https://api.github.com/repos/clangd/clangd/releases/latest")
"https://api.github.com/repos/clangd/clangd/releases/latest",
)
if (!releaseResponse.ok) { if (!releaseResponse.ok) {
log.error("Failed to fetch clangd release info") log.error("Failed to fetch clangd release info")
return return
@@ -723,24 +674,12 @@ export namespace LSPServer {
export const Svelte: Info = { export const Svelte: Info = {
id: "svelte", id: "svelte",
extensions: [".svelte"], extensions: [".svelte"],
root: NearestRoot([ root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
"package-lock.json",
"bun.lockb",
"bun.lock",
"pnpm-lock.yaml",
"yarn.lock",
]),
async spawn(root) { async spawn(root) {
let binary = Bun.which("svelteserver") let binary = Bun.which("svelteserver")
const args: string[] = [] const args: string[] = []
if (!binary) { if (!binary) {
const js = path.join( const js = path.join(Global.Path.bin, "node_modules", "svelte-language-server", "bin", "server.js")
Global.Path.bin,
"node_modules",
"svelte-language-server",
"bin",
"server.js",
)
if (!(await Bun.file(js).exists())) { if (!(await Bun.file(js).exists())) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
await Bun.spawn([BunProc.which(), "install", "svelte-language-server"], { await Bun.spawn([BunProc.which(), "install", "svelte-language-server"], {
@@ -775,17 +714,9 @@ export namespace LSPServer {
export const Astro: Info = { export const Astro: Info = {
id: "astro", id: "astro",
extensions: [".astro"], extensions: [".astro"],
root: NearestRoot([ root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
"package-lock.json",
"bun.lockb",
"bun.lock",
"pnpm-lock.yaml",
"yarn.lock",
]),
async spawn(root) { async spawn(root) {
const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch( const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(() => {})
() => {},
)
if (!tsserver) { if (!tsserver) {
log.info("typescript not found, required for Astro language server") log.info("typescript not found, required for Astro language server")
return return
@@ -795,14 +726,7 @@ export namespace LSPServer {
let binary = Bun.which("astro-ls") let binary = Bun.which("astro-ls")
const args: string[] = [] const args: string[] = []
if (!binary) { if (!binary) {
const js = path.join( const js = path.join(Global.Path.bin, "node_modules", "@astrojs", "language-server", "bin", "nodeServer.js")
Global.Path.bin,
"node_modules",
"@astrojs",
"language-server",
"bin",
"nodeServer.js",
)
if (!(await Bun.file(js).exists())) { if (!(await Bun.file(js).exists())) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
await Bun.spawn([BunProc.which(), "install", "@astrojs/language-server"], { await Bun.spawn([BunProc.which(), "install", "@astrojs/language-server"], {
@@ -880,9 +804,7 @@ export namespace LSPServer {
.then(({ stdout }) => stdout.toString().trim()) .then(({ stdout }) => stdout.toString().trim())
const launcherJar = path.join(launcherDir, jarFileName) const launcherJar = path.join(launcherDir, jarFileName)
if (!(await fs.exists(launcherJar))) { if (!(await fs.exists(launcherJar))) {
log.error( log.error(`Failed to locate the JDTLS launcher module in the installed directory: ${distPath}.`)
`Failed to locate the JDTLS launcher module in the installed directory: ${distPath}.`,
)
return return
} }
const configFile = path.join( const configFile = path.join(
@@ -948,9 +870,7 @@ export namespace LSPServer {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("downloading lua-language-server from GitHub releases") log.info("downloading lua-language-server from GitHub releases")
const releaseResponse = await fetch( const releaseResponse = await fetch("https://api.github.com/repos/LuaLS/lua-language-server/releases/latest")
"https://api.github.com/repos/LuaLS/lua-language-server/releases/latest",
)
if (!releaseResponse.ok) { if (!releaseResponse.ok) {
log.error("Failed to fetch lua-language-server release info") log.error("Failed to fetch lua-language-server release info")
return return
@@ -987,9 +907,7 @@ export namespace LSPServer {
const assetSuffix = `${lualsPlatform}-${lualsArch}.${ext}` const assetSuffix = `${lualsPlatform}-${lualsArch}.${ext}`
if (!supportedCombos.includes(assetSuffix)) { if (!supportedCombos.includes(assetSuffix)) {
log.error( log.error(`Platform ${platform} and architecture ${arch} is not supported by lua-language-server`)
`Platform ${platform} and architecture ${arch} is not supported by lua-language-server`,
)
return return
} }
@@ -1012,10 +930,7 @@ export namespace LSPServer {
// Unlike zls which is a single self-contained binary, // Unlike zls which is a single self-contained binary,
// lua-language-server needs supporting files (meta/, locale/, etc.) // lua-language-server needs supporting files (meta/, locale/, etc.)
// Extract entire archive to dedicated directory to preserve all files // Extract entire archive to dedicated directory to preserve all files
const installDir = path.join( const installDir = path.join(Global.Path.bin, `lua-language-server-${lualsArch}-${lualsPlatform}`)
Global.Path.bin,
`lua-language-server-${lualsArch}-${lualsPlatform}`,
)
// Remove old installation if exists // Remove old installation if exists
const stats = await fs.stat(installDir).catch(() => undefined) const stats = await fs.stat(installDir).catch(() => undefined)
@@ -1040,11 +955,7 @@ export namespace LSPServer {
await fs.rm(tempPath, { force: true }) await fs.rm(tempPath, { force: true })
// Binary is located in bin/ subdirectory within the extracted archive // Binary is located in bin/ subdirectory within the extracted archive
bin = path.join( bin = path.join(installDir, "bin", "lua-language-server" + (platform === "win32" ? ".exe" : ""))
installDir,
"bin",
"lua-language-server" + (platform === "win32" ? ".exe" : ""),
)
if (!(await Bun.file(bin).exists())) { if (!(await Bun.file(bin).exists())) {
log.error("Failed to extract lua-language-server binary") log.error("Failed to extract lua-language-server binary")

View File

@@ -104,10 +104,7 @@ export namespace Patch {
return null return null
} }
function parseUpdateFileChunks( function parseUpdateFileChunks(lines: string[], startIdx: number): { chunks: UpdateFileChunk[]; nextIdx: number } {
lines: string[],
startIdx: number,
): { chunks: UpdateFileChunk[]; nextIdx: number } {
const chunks: UpdateFileChunk[] = [] const chunks: UpdateFileChunk[] = []
let i = startIdx let i = startIdx
@@ -161,10 +158,7 @@ export namespace Patch {
return { chunks, nextIdx: i } return { chunks, nextIdx: i }
} }
function parseAddFileContent( function parseAddFileContent(lines: string[], startIdx: number): { content: string; nextIdx: number } {
lines: string[],
startIdx: number,
): { content: string; nextIdx: number } {
let content = "" let content = ""
let i = startIdx let i = startIdx
@@ -303,10 +297,7 @@ export namespace Patch {
content: string content: string
} }
export function deriveNewContentsFromChunks( export function deriveNewContentsFromChunks(filePath: string, chunks: UpdateFileChunk[]): ApplyPatchFileUpdate {
filePath: string,
chunks: UpdateFileChunk[],
): ApplyPatchFileUpdate {
// Read original file content // Read original file content
let originalContent: string let originalContent: string
try { try {
@@ -387,9 +378,7 @@ export namespace Patch {
replacements.push([found, pattern.length, newSlice]) replacements.push([found, pattern.length, newSlice])
lineIndex = found + pattern.length lineIndex = found + pattern.length
} else { } else {
throw new Error( throw new Error(`Failed to find expected lines in ${filePath}:\n${chunk.old_lines.join("\n")}`)
`Failed to find expected lines in ${filePath}:\n${chunk.old_lines.join("\n")}`,
)
} }
} }
@@ -399,10 +388,7 @@ export namespace Patch {
return replacements return replacements
} }
function applyReplacements( function applyReplacements(lines: string[], replacements: Array<[number, number, string[]]>): string[] {
lines: string[],
replacements: Array<[number, number, string[]]>,
): string[] {
// Apply replacements in reverse order to avoid index shifting // Apply replacements in reverse order to avoid index shifting
const result = [...lines] const result = [...lines]
@@ -601,9 +587,7 @@ export namespace Patch {
changes.set(resolvedPath, { changes.set(resolvedPath, {
type: "update", type: "update",
unified_diff: fileUpdate.unified_diff, unified_diff: fileUpdate.unified_diff,
move_path: hunk.move_path move_path: hunk.move_path ? path.resolve(effectiveCwd, hunk.move_path) : undefined,
? path.resolve(effectiveCwd, hunk.move_path)
: undefined,
new_content: fileUpdate.content, new_content: fileUpdate.content,
}) })
} catch (error) { } catch (error) {

View File

@@ -75,14 +75,7 @@ export namespace Permission {
async (state) => { async (state) => {
for (const pending of Object.values(state.pending)) { for (const pending of Object.values(state.pending)) {
for (const item of Object.values(pending)) { for (const item of Object.values(pending)) {
item.reject( item.reject(new RejectedError(item.info.sessionID, item.info.id, item.info.callID, item.info.metadata))
new RejectedError(
item.info.sessionID,
item.info.id,
item.info.callID,
item.info.metadata,
),
)
} }
} }
}, },
@@ -150,11 +143,7 @@ export namespace Permission {
export const Response = z.enum(["once", "always", "reject"]) export const Response = z.enum(["once", "always", "reject"])
export type Response = z.infer<typeof Response> export type Response = z.infer<typeof Response>
export function respond(input: { export function respond(input: { sessionID: Info["sessionID"]; permissionID: Info["id"]; response: Response }) {
sessionID: Info["sessionID"]
permissionID: Info["id"]
response: Response
}) {
log.info("response", input) log.info("response", input)
const { pending, approved } = state() const { pending, approved } = state()
const match = pending[input.sessionID]?.[input.permissionID] const match = pending[input.sessionID]?.[input.permissionID]
@@ -166,14 +155,7 @@ export namespace Permission {
response: input.response, response: input.response,
}) })
if (input.response === "reject") { if (input.response === "reject") {
match.reject( match.reject(new RejectedError(input.sessionID, input.permissionID, match.info.callID, match.info.metadata))
new RejectedError(
input.sessionID,
input.permissionID,
match.info.callID,
match.info.metadata,
),
)
return return
} }
match.resolve() match.resolve()
@@ -205,9 +187,7 @@ export namespace Permission {
public readonly toolCallID?: string, public readonly toolCallID?: string,
public readonly metadata?: Record<string, any>, public readonly metadata?: Record<string, any>,
) { ) {
super( super(`The user rejected permission to use this specific tool call. You may try again with different parameters.`)
`The user rejected permission to use this specific tool call. You may try again with different parameters.`,
)
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show More