summaryrefslogtreecommitdiffhomepage
path: root/cloud
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-08-15 19:29:24 -0400
committerDax Raad <[email protected]>2025-08-15 19:29:42 -0400
commit07cf8847fb1908ff5dc47a771f57d23926baa1ce (patch)
treeaef73a8ac42e755404cb56107137a7fc4eff3ffd /cloud
parent650e67f1dfd4790152c70864da6c1ade4884ab58 (diff)
downloadopencode-07cf8847fb1908ff5dc47a771f57d23926baa1ce.tar.gz
opencode-07cf8847fb1908ff5dc47a771f57d23926baa1ce.zip
wip: cloud stuff
Diffstat (limited to 'cloud')
-rw-r--r--cloud/app/.gitignore28
-rw-r--r--cloud/app/.opencode/agent/css.md61
-rw-r--r--cloud/app/README.md32
-rw-r--r--cloud/app/app.config.ts9
-rw-r--r--cloud/app/package.json21
-rw-r--r--cloud/app/public/favicon.icobin0 -> 664 bytes
-rw-r--r--cloud/app/src/app.css1
-rw-r--r--cloud/app/src/app.tsx21
-rw-r--r--cloud/app/src/asset/logo-ornate-dark.svg19
-rw-r--r--cloud/app/src/asset/screenshot-github.webpbin0 -> 924094 bytes
-rw-r--r--cloud/app/src/asset/screenshot-splash.webpbin0 -> 467281 bytes
-rw-r--r--cloud/app/src/asset/screenshot-vscode.webpbin0 -> 1022418 bytes
-rw-r--r--cloud/app/src/component/icon.tsx24
-rw-r--r--cloud/app/src/entry-client.tsx4
-rw-r--r--cloud/app/src/entry-server.tsx21
-rw-r--r--cloud/app/src/global.d.ts1
-rw-r--r--cloud/app/src/routes/[...404].tsx19
-rw-r--r--cloud/app/src/routes/index.css264
-rw-r--r--cloud/app/src/routes/index.tsx169
-rw-r--r--cloud/app/src/style/base.css8
-rw-r--r--cloud/app/src/style/component/button.css102
-rw-r--r--cloud/app/src/style/index.css8
-rw-r--r--cloud/app/src/style/reset.css76
-rw-r--r--cloud/app/src/style/token/color.css90
-rw-r--r--cloud/app/src/style/token/font.css18
-rw-r--r--cloud/app/src/style/token/space.css41
-rw-r--r--cloud/app/tsconfig.json19
-rw-r--r--cloud/web/src/ui/style/token/space.css1
28 files changed, 1057 insertions, 0 deletions
diff --git a/cloud/app/.gitignore b/cloud/app/.gitignore
new file mode 100644
index 000000000..751513ce1
--- /dev/null
+++ b/cloud/app/.gitignore
@@ -0,0 +1,28 @@
+dist
+.wrangler
+.output
+.vercel
+.netlify
+.vinxi
+app.config.timestamp_*.js
+
+# Environment
+.env
+.env*.local
+
+# dependencies
+/node_modules
+
+# IDEs and editors
+/.idea
+.project
+.classpath
+*.launch
+.settings/
+
+# Temp
+gitignore
+
+# System Files
+.DS_Store
+Thumbs.db
diff --git a/cloud/app/.opencode/agent/css.md b/cloud/app/.opencode/agent/css.md
new file mode 100644
index 000000000..58c59da73
--- /dev/null
+++ b/cloud/app/.opencode/agent/css.md
@@ -0,0 +1,61 @@
+---
+description: use whenever you are styling a ui with css
+---
+
+you are very good at writing clean maintainable css using modern techniques
+
+css is structured like this
+
+```css
+[data-page="home"] {
+ [data-component="header"] {
+ [data-slot="logo"] {
+ }
+ }
+}
+```
+
+top level pages are scoped using `data-page`
+
+pages can break down into components using `data-component`
+
+components can break down into slots using `data-slot`
+
+structure things so that this hierarchy is followed - you should rarely need to
+nest components inside other components. you should NEVER nest components inside
+slots. you should NEVER nest slots inside other slots.
+
+thei hierarchy in css file does NOT have to match the hierarchy in the dom - you
+can put components or slots at the same level even if one goes inside another.
+
+it is more important to follow the pages -> components -> slots structure
+
+use data attributes to represent different states of the component
+
+```css
+[data-component="modal"] {
+ opacity: 0;
+
+ &[data-state="open"] {
+ opacity: 1;
+ }
+}
+```
+
+this will allow jsx to control the syling
+
+avoid selectors that just target an element type like `> span` you should assign
+it a slot name. it's ok to do this sometimes where it makes sense semantically
+like targeting `li` elements in a list
+
+in terms of file structure `./src/style/` contains all universal styling rules.
+these should not contain anything specific to a page
+
+`./src/style/token` contains all the tokens used in the project
+
+`./src/style/component` is for reusable components like buttons or inputs
+
+page specific styles should go next to the page they are styling so
+`./src/routes/about.tsx` should have its styles in `./src/routes/about.css`
+
+`about.css` should be scoped using `data-page="about"`
diff --git a/cloud/app/README.md b/cloud/app/README.md
new file mode 100644
index 000000000..9337430cf
--- /dev/null
+++ b/cloud/app/README.md
@@ -0,0 +1,32 @@
+# SolidStart
+
+Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com);
+
+## Creating a project
+
+```bash
+# create a new project in the current directory
+npm init solid@latest
+
+# create a new project in my-app
+npm init solid@latest my-app
+```
+
+## Developing
+
+Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
+
+```bash
+npm run dev
+
+# or start the server and open the app in a new browser tab
+npm run dev -- --open
+```
+
+## Building
+
+Solid apps are built with _presets_, which optimise your project for deployment to different environments.
+
+By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add it to the `devDependencies` in `package.json` and specify in your `app.config.js`.
+
+## This project was created with the [Solid CLI](https://github.com/solidjs-community/solid-cli)
diff --git a/cloud/app/app.config.ts b/cloud/app/app.config.ts
new file mode 100644
index 000000000..0ffa557f9
--- /dev/null
+++ b/cloud/app/app.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from "@solidjs/start/config"
+
+export default defineConfig({
+ vite: {
+ server: {
+ allowedHosts: true,
+ },
+ },
+})
diff --git a/cloud/app/package.json b/cloud/app/package.json
new file mode 100644
index 000000000..59556e0ba
--- /dev/null
+++ b/cloud/app/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@opencode/cloud-app",
+ "type": "module",
+ "scripts": {
+ "dev": "vinxi dev --host 0.0.0.0",
+ "build": "vinxi build",
+ "start": "vinxi start",
+ "version": "vinxi version"
+ },
+ "dependencies": {
+ "@ibm/plex": "6.4.1",
+ "@solidjs/meta": "^0.29.4",
+ "@solidjs/router": "^0.15.0",
+ "@solidjs/start": "^1.1.0",
+ "solid-js": "^1.9.5",
+ "vinxi": "^0.5.7"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+}
diff --git a/cloud/app/public/favicon.ico b/cloud/app/public/favicon.ico
new file mode 100644
index 000000000..fb282da07
--- /dev/null
+++ b/cloud/app/public/favicon.ico
Binary files differ
diff --git a/cloud/app/src/app.css b/cloud/app/src/app.css
new file mode 100644
index 000000000..c0261c422
--- /dev/null
+++ b/cloud/app/src/app.css
@@ -0,0 +1 @@
+@import "./style/index.css";
diff --git a/cloud/app/src/app.tsx b/cloud/app/src/app.tsx
new file mode 100644
index 000000000..04c569b91
--- /dev/null
+++ b/cloud/app/src/app.tsx
@@ -0,0 +1,21 @@
+import { MetaProvider, Title } from "@solidjs/meta";
+import { Router } from "@solidjs/router";
+import { FileRoutes } from "@solidjs/start/router";
+import { Suspense } from "solid-js";
+import "@ibm/plex/css/ibm-plex.css";
+import "./app.css";
+
+export default function App() {
+ return (
+ <Router
+ root={props => (
+ <MetaProvider>
+ <Title>SolidStart - Basic</Title>
+ <Suspense>{props.children}</Suspense>
+ </MetaProvider>
+ )}
+ >
+ <FileRoutes />
+ </Router>
+ );
+}
diff --git a/cloud/app/src/asset/logo-ornate-dark.svg b/cloud/app/src/asset/logo-ornate-dark.svg
new file mode 100644
index 000000000..2efda934d
--- /dev/null
+++ b/cloud/app/src/asset/logo-ornate-dark.svg
@@ -0,0 +1,19 @@
+<svg width="289" height="50" viewBox="0 0 289 50" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8.5 16.5H24.5V33H8.5V16.5Z" fill="white" fill-opacity="0.2"/>
+<path d="M48.5 16.5H64.5V33H48.5V16.5Z" fill="white" fill-opacity="0.2"/>
+<path d="M120.5 16.5H136.5V33H120.5V16.5Z" fill="white" fill-opacity="0.2"/>
+<path d="M160.5 16.5H176.5V33H160.5V16.5Z" fill="white" fill-opacity="0.2"/>
+<path d="M192.5 16.5H208.5V33H192.5V16.5Z" fill="white" fill-opacity="0.2"/>
+<path d="M232.5 16.5H248.5V33H232.5V16.5Z" fill="white" fill-opacity="0.2"/>
+<path d="M264.5 0H288.5V8.5H272.5V16.5H288.5V25H272.5V33H288.5V41.5H264.5V0Z" fill="white" fill-opacity="0.95"/>
+<path d="M248.5 0H224.5V41.5H248.5V33H232.5V8.5H248.5V0Z" fill="white" fill-opacity="0.95"/>
+<path d="M256.5 8.5H248.5V33H256.5V8.5Z" fill="white" fill-opacity="0.95"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M184.5 0H216.5V41.5H184.5V0ZM208.5 8.5H192.5V33H208.5V8.5Z" fill="white" fill-opacity="0.95"/>
+<path d="M144.5 8.5H136.5V41.5H144.5V8.5Z" fill="white" fill-opacity="0.5"/>
+<path d="M136.5 0H112.5V41.5H120.5V8.5H136.5V0Z" fill="white" fill-opacity="0.5"/>
+<path d="M80.5 0H104.5V8.5H88.5V16.5H104.5V25H88.5V33H104.5V41.5H80.5V0Z" fill="white" fill-opacity="0.5"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M40.5 0H72.5V41.5H48.5V49.5H40.5V0ZM64.5 8.5H48.5V33H64.5V8.5Z" fill="white" fill-opacity="0.5"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M0.5 0H32.5V41.5955H0.5V0ZM24.5 8.5H8.5V33H24.5V8.5Z" fill="white" fill-opacity="0.5"/>
+<path d="M152.5 0H176.5V8.5H160.5V33H176.5V41.5H152.5V0Z" fill="white" fill-opacity="0.95"/>
+</svg>
+
diff --git a/cloud/app/src/asset/screenshot-github.webp b/cloud/app/src/asset/screenshot-github.webp
new file mode 100644
index 000000000..fda74e641
--- /dev/null
+++ b/cloud/app/src/asset/screenshot-github.webp
Binary files differ
diff --git a/cloud/app/src/asset/screenshot-splash.webp b/cloud/app/src/asset/screenshot-splash.webp
new file mode 100644
index 000000000..e900673ef
--- /dev/null
+++ b/cloud/app/src/asset/screenshot-splash.webp
Binary files differ
diff --git a/cloud/app/src/asset/screenshot-vscode.webp b/cloud/app/src/asset/screenshot-vscode.webp
new file mode 100644
index 000000000..b8966a6b8
--- /dev/null
+++ b/cloud/app/src/asset/screenshot-vscode.webp
Binary files differ
diff --git a/cloud/app/src/component/icon.tsx b/cloud/app/src/component/icon.tsx
new file mode 100644
index 000000000..5a565ab9a
--- /dev/null
+++ b/cloud/app/src/component/icon.tsx
@@ -0,0 +1,24 @@
+
+import { JSX } from "solid-js"
+
+
+export function IconCopy(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 512 512" >
+ <rect width="336" height="336" x="128" y="128" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32" rx="57" ry="57"></rect>
+ <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="m383.5 128l.5-24a56.16 56.16 0 0 0-56-56H112a64.19 64.19 0 0 0-64 64v216a56.16 56.16 0 0 0 56 56h24"></path>
+ </svg>
+ )
+}
+
+export function IconCheck(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24" >
+ <path fill="currentColor" d="M9 16.17L5.53 12.7a.996.996 0 1 0-1.41 1.41l4.18 4.18c.39.39 1.02.39 1.41 0L20.29 7.71a.996.996 0 1 0-1.41-1.41z"></path>
+ </svg>
+ )
+}
diff --git a/cloud/app/src/entry-client.tsx b/cloud/app/src/entry-client.tsx
new file mode 100644
index 000000000..0ca4e3c30
--- /dev/null
+++ b/cloud/app/src/entry-client.tsx
@@ -0,0 +1,4 @@
+// @refresh reload
+import { mount, StartClient } from "@solidjs/start/client";
+
+mount(() => <StartClient />, document.getElementById("app")!);
diff --git a/cloud/app/src/entry-server.tsx b/cloud/app/src/entry-server.tsx
new file mode 100644
index 000000000..eb8aea1e8
--- /dev/null
+++ b/cloud/app/src/entry-server.tsx
@@ -0,0 +1,21 @@
+// @refresh reload
+import { createHandler, StartServer } from "@solidjs/start/server";
+
+export default createHandler(() => (
+ <StartServer
+ document={({ assets, children, scripts }) => (
+ <html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <link rel="icon" href="/favicon.ico" />
+ {assets}
+ </head>
+ <body data-color-mode="dark">
+ <div id="app">{children}</div>
+ {scripts}
+ </body>
+ </html>
+ )}
+ />
+));
diff --git a/cloud/app/src/global.d.ts b/cloud/app/src/global.d.ts
new file mode 100644
index 000000000..dc6f10c22
--- /dev/null
+++ b/cloud/app/src/global.d.ts
@@ -0,0 +1 @@
+/// <reference types="@solidjs/start/env" />
diff --git a/cloud/app/src/routes/[...404].tsx b/cloud/app/src/routes/[...404].tsx
new file mode 100644
index 000000000..4ea71ec7f
--- /dev/null
+++ b/cloud/app/src/routes/[...404].tsx
@@ -0,0 +1,19 @@
+import { Title } from "@solidjs/meta";
+import { HttpStatusCode } from "@solidjs/start";
+
+export default function NotFound() {
+ return (
+ <main>
+ <Title>Not Found</Title>
+ <HttpStatusCode code={404} />
+ <h1>Page Not Found</h1>
+ <p>
+ Visit{" "}
+ <a href="https://start.solidjs.com" target="_blank">
+ start.solidjs.com
+ </a>{" "}
+ to learn how to build SolidStart apps.
+ </p>
+ </main>
+ );
+}
diff --git a/cloud/app/src/routes/index.css b/cloud/app/src/routes/index.css
new file mode 100644
index 000000000..e3b11c605
--- /dev/null
+++ b/cloud/app/src/routes/index.css
@@ -0,0 +1,264 @@
+[data-page="home"] {
+ --color-bg: oklch(0.2097 0.008 274.53);
+ --color-border: oklch(0.46 0.02 269.88);
+ --color-text: #ffffff;
+ --color-text-secondary: oklch(0.72 0.01 270.15);
+ --color-text-dimmed: hsl(224, 7%, 46%);
+ padding: var(--space-6);
+ font-family: var(--font-mono);
+ color: var(--color-text);
+
+ a {
+ color: var(--color-text);
+ text-decoration: underline;
+ text-underline-offset: 0.1875rem;
+ }
+
+ background: var(--color-bg);
+ position: fixed;
+ overflow-y: scroll;
+ inset: 0;
+
+ [data-component="content"] {
+ max-width: 67.5rem;
+ margin: 0 auto;
+ border: 2px solid var(--color-border);
+ }
+
+ [data-component="top"] {
+ padding: var(--space-12);
+ display: flex;
+ flex-direction: column;
+ align-items: start;
+ gap: var(--space-4);
+
+ [data-slot="logo"] {
+ height: 70px;
+ }
+
+ [data-slot="title"] {
+ font-size: var(--font-size-2xl);
+ text-transform: uppercase;
+ }
+ }
+
+ [data-component="cta"] {
+ height: var(--space-19);
+ border-top: 2px solid var(--color-border);
+ display: flex;
+
+ [data-slot="left"] {
+ display: flex;
+ padding: 0 var(--space-12);
+ text-transform: uppercase;
+ text-decoration: underline;
+ align-items: center;
+ justify-content: center;
+ text-underline-offset: 0.1875rem;
+ border-right: 2px solid var(--color-border);
+
+ a {
+ color: var(--color-text);
+ text-decoration: underline;
+ }
+ }
+
+ [data-slot="right"] {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.625rem;
+ padding: 0 var(--space-6);
+ }
+
+ [data-slot="command"] {
+ all: unset;
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ color: var(--color-text-secondary);
+ font-size: 1.125rem;
+ font-family: var(--font-mono);
+ gap: var(--space-2);
+ }
+
+ [data-slot="highlight"] {
+ color: var(--color-text);
+ font-weight: 500;
+ }
+ }
+
+ [data-component="features"] {
+ border-top: 2px solid var(--color-border);
+ padding: var(--space-12);
+
+ [data-slot="list"] {
+ padding-left: var(--space-4);
+ margin: 0;
+ list-style: disc;
+
+ li {
+ margin-bottom: var(--space-4);
+
+ strong {
+ text-transform: uppercase;
+ font-weight: 600;
+ }
+ }
+
+ li:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ [data-component="install"] {
+ border-top: 2px solid var(--color-border);
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-template-rows: 1fr 1fr;
+
+ @media (max-width: 40rem) {
+ grid-template-columns: 1fr;
+ grid-template-rows: auto;
+ }
+ }
+
+ [data-component="title"] {
+ letter-spacing: -0.03125rem;
+ text-transform: uppercase;
+ font-weight: 400;
+ font-size: var(--font-size-md);
+ flex-shrink: 0;
+ color: oklch(0.55 0.02 269.87);
+ }
+
+ [data-component="method"] {
+ padding: var(--space-4) var(--space-6);
+ display: flex;
+ flex-direction: column;
+ align-items: start;
+ gap: var(--space-3);
+
+ &:nth-child(1) {}
+
+ &:nth-child(2) {
+ border-left: 2px solid var(--color-border);
+ }
+
+ &:nth-child(3) {
+ border-top: 2px solid var(--color-border);
+ }
+
+ &:nth-child(4) {
+ border-top: 2px solid var(--color-border);
+ border-left: 2px solid var(--color-border);
+ }
+
+ [data-slot="button"] {
+ all: unset;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ color: var(--color-text-secondary);
+ gap: var(--space-2);
+
+ strong {
+ color: var(--color-text);
+ font-weight: 500;
+ }
+ }
+ }
+
+ [data-component="screenshots"] {
+ border-top: 2px solid var(--color-border);
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 0;
+
+ [data-slot="left"] {
+ padding: var(--space-8) var(--space-6);
+ display: flex;
+ flex-direction: column;
+
+ img {
+ width: 100%;
+ height: "auto";
+ }
+ }
+
+ [data-slot="right"] {
+ display: grid;
+ grid-template-rows: 1fr 1fr;
+ border-left: 2px solid var(--color-border);
+ }
+
+ [data-slot="filler"] {
+ display: flex;
+ flex-grow: 1;
+ align-items: center;
+ justify-content: center;
+ }
+
+ [data-slot="cell"] {
+ padding: var(--space-8) var(--space-6);
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-4);
+
+ &:nth-child(2) {
+ border-top: 2px solid var(--color-border);
+ }
+
+ img {
+ width: 80%;
+ height: "auto";
+ }
+ }
+ }
+
+ [data-component="copy-status"] {
+ [data-slot="copy"] {
+ display: block;
+ width: 16px;
+ height: 16px;
+ color: var(--color-text-dimmed);
+
+ [data-copied] & {
+ display: none;
+ }
+ }
+
+ [data-slot="check"] {
+ display: none;
+ width: 16px;
+ height: 16px;
+ color: white;
+
+ [data-copied] & {
+ display: block;
+ }
+ }
+ }
+
+ [data-component="footer"] {
+ border-top: 2px solid var(--color-border);
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ font-size: var(--font-size-lg);
+ height: var(--space-20);
+
+ [data-slot="cell"] {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-right: 2px solid var(--color-border);
+ text-transform: uppercase;
+
+ &:last-child {
+ border-right: none;
+ }
+ }
+ }
+}
diff --git a/cloud/app/src/routes/index.tsx b/cloud/app/src/routes/index.tsx
new file mode 100644
index 000000000..90062d0b5
--- /dev/null
+++ b/cloud/app/src/routes/index.tsx
@@ -0,0 +1,169 @@
+import { Title } from "@solidjs/meta"
+import { onCleanup, onMount } from "solid-js"
+import "./index.css"
+import logo from "../asset/logo-ornate-dark.svg"
+import IMG_SPLASH from "../asset/screenshot-splash.webp"
+import IMG_VSCODE from "../asset/screenshot-vscode.webp"
+import IMG_GITHUB from "../asset/screenshot-github.webp"
+import { IconCopy, IconCheck } from "../component/icon"
+
+function CopyStatus() {
+ return (
+ <div data-component="copy-status">
+ <IconCopy data-slot="copy" />
+ <IconCheck data-slot="check" />
+ </div>
+ )
+}
+
+export default function Home() {
+ onMount(() => {
+ const commands = document.querySelectorAll("[data-copy]")
+ for (const button of commands) {
+ const callback = () => {
+ const text = button.textContent
+ alert(text)
+ if (text) {
+ navigator.clipboard.writeText(text)
+ button.setAttribute("data-copied", "")
+ setTimeout(() => {
+ button.removeAttribute("data-copied")
+ }, 1500)
+ }
+ }
+ button.addEventListener("click", callback)
+ onCleanup(() => {
+ button.removeEventListener("click", callback)
+ })
+ }
+ })
+
+ return (
+ <main data-page="home">
+ <Title>opencode | AI coding agent built for the terminal</Title>
+ <div data-component="content">
+ <section data-component="top">
+ <img data-slot="logo" src={logo} alt="logo" />
+ <h1 data-slot="title">The AI coding agent built for the terminal.</h1>
+ </section>
+
+ <section data-component="cta">
+ <div data-slot="left">
+ <a href="/docs">Get Started</a>
+ </div>
+ <div data-slot="right">
+ <button data-copy data-slot="command" data-command="curl -fsSL https://opencode.ai/install | bash">
+ <span>
+ <span>curl -fsSL&nbsp;</span>
+ <span data-slot="protocol">https://</span>
+ <span data-slot="highlight">opencode.ai/install</span>
+ &nbsp;| bash
+ </span>
+ <CopyStatus />
+ </button>
+ </div>
+ </section>
+
+ <section data-component="features">
+ <ul data-slot="list">
+ <li>
+ <strong>Native TUI</strong>: A responsive, native, themeable terminal UI.
+ </li>
+ <li>
+ <strong>LSP enabled</strong>: Automatically loads the right LSPs for the LLM.
+ </li>
+ <li>
+ <strong>Multi-session</strong>: Start multiple agents in parallel on the same project.
+ </li>
+ <li>
+ <strong>Shareable links</strong>: Share a link to any sessions for reference or to debug.
+ </li>
+ <li>
+ <strong>Claude Pro</strong>: Log in with Anthropic to use your Claude Pro or Max account.
+ </li>
+ <li>
+ <strong>Use any model</strong>: Supports 75+ LLM providers through{" "}
+ <a href="https://models.dev">Models.dev</a>, including local models.
+ </li>
+ </ul>
+ </section>
+
+ <section data-component="install">
+ <div data-component="method">
+ <h3 data-component="title">npm</h3>
+ <button data-copy data-slot="button">
+ <span>
+ npm install -g&nbsp;<strong>opencode-ai</strong>
+ </span>
+ <CopyStatus />
+ </button>
+ </div>
+ <div data-component="method">
+ <h3 data-component="title">bun</h3>
+ <button data-copy data-slot="button">
+ <span>
+ bun install -g&nbsp;<strong>opencode-ai</strong>
+ </span>
+ <CopyStatus />
+ </button>
+ </div>
+ <div data-component="method">
+ <h3 data-component="title">homebrew</h3>
+ <button data-copy data-slot="button">
+ <span>
+ brew install&nbsp;<strong>sst/tap/opencode</strong>
+ </span>
+ <CopyStatus />
+ </button>
+ </div>
+ <div data-component="method">
+ <h3 data-component="title">paru</h3>
+ <button data-copy data-slot="button">
+ <span>
+ paru -S&nbsp;<strong>opencode-bin</strong>
+ </span>
+ <CopyStatus />
+ </button>
+ </div>
+ </section>
+
+ <section data-component="screenshots">
+ <div data-slot="left">
+ <div data-component="title">opencode TUI with tokyonight theme</div>
+ <div data-slot="filler">
+ <img src={IMG_SPLASH} alt="opencode TUI with tokyonight theme" />
+ </div>
+ </div>
+ <div data-slot="right">
+ <div data-slot="cell">
+ <div data-component="title">opencode in VS Code</div>
+ <div data-slot="filler">
+ <img src={IMG_VSCODE} alt="opencode in VS Code" />
+ </div>
+ </div>
+ <div data-slot="cell">
+ <div data-component="title">opencode in GitHub</div>
+ <div data-slot="filler">
+ <img src={IMG_GITHUB} alt="opencode in GitHub" />
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <footer data-component="footer">
+ <div data-slot="cell">
+ <a href="https://github.com/sst/opencode">GitHub</a>
+ </div>
+ <div data-slot="cell">
+ <a href="https://opencode.ai/discord">Discord</a>
+ </div>
+ <div data-slot="cell">
+ <span>
+ ©2025 <a href="https://anoma.ly">Anomaly Innovations</a>
+ </span>
+ </div>
+ </footer>
+ </div>
+ </main>
+ )
+}
diff --git a/cloud/app/src/style/base.css b/cloud/app/src/style/base.css
new file mode 100644
index 000000000..2c95cdbb7
--- /dev/null
+++ b/cloud/app/src/style/base.css
@@ -0,0 +1,8 @@
+html {
+ color-scheme: dark;
+ line-height: 1;
+}
+
+body {
+ font-family: var(--font-sans);
+}
diff --git a/cloud/app/src/style/component/button.css b/cloud/app/src/style/component/button.css
new file mode 100644
index 000000000..d10f7af53
--- /dev/null
+++ b/cloud/app/src/style/component/button.css
@@ -0,0 +1,102 @@
+[data-component="button"] {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--space-2);
+ padding: var(--space-3) var(--space-4);
+ border: 1px solid transparent;
+ border-radius: var(--space-2);
+ font-family: var(--font-sans);
+ font-size: var(--font-size-md);
+ font-weight: 500;
+ line-height: 1.25;
+ cursor: pointer;
+ transition: all 0.2s ease-in-out;
+ text-decoration: none;
+ user-select: none;
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
+ &:focus {
+ outline: none;
+ box-shadow: 0 0 0 2px var(--color-primary);
+ }
+
+ &[data-color="primary"] {
+ background-color: var(--color-primary);
+ color: var(--color-primary-text);
+ border-color: var(--color-primary);
+
+ &:hover:not(:disabled) {
+ background-color: var(--color-primary-hover);
+ border-color: var(--color-primary-hover);
+ }
+
+ &:active:not(:disabled) {
+ background-color: var(--color-primary-active);
+ border-color: var(--color-primary-active);
+ }
+ }
+
+ &[data-color="danger"] {
+ background-color: var(--color-danger);
+ color: var(--color-danger-text);
+ border-color: var(--color-danger);
+
+ &:hover:not(:disabled) {
+ background-color: var(--color-danger-hover);
+ border-color: var(--color-danger-hover);
+ }
+
+ &:active:not(:disabled) {
+ background-color: var(--color-danger-active);
+ border-color: var(--color-danger-active);
+ }
+
+ &:focus {
+ box-shadow: 0 0 0 2px var(--color-danger);
+ }
+ }
+
+ &[data-color="warning"] {
+ background-color: var(--color-warning);
+ color: var(--color-warning-text);
+ border-color: var(--color-warning);
+
+ &:hover:not(:disabled) {
+ background-color: var(--color-warning-hover);
+ border-color: var(--color-warning-hover);
+ }
+
+ &:active:not(:disabled) {
+ background-color: var(--color-warning-active);
+ border-color: var(--color-warning-active);
+ }
+
+ &:focus {
+ box-shadow: 0 0 0 2px var(--color-warning);
+ }
+ }
+
+ &[data-size="small"] {
+ padding: var(--space-2) var(--space-3);
+ font-size: var(--font-size-sm);
+ gap: var(--space-1-5);
+ }
+
+ &[data-size="large"] {
+ padding: var(--space-4) var(--space-6);
+ font-size: var(--font-size-lg);
+ gap: var(--space-3);
+ }
+
+ [data-slot="icon"] {
+ display: flex;
+ align-items: center;
+ width: 1em;
+ height: 1em;
+ }
+}
diff --git a/cloud/app/src/style/index.css b/cloud/app/src/style/index.css
new file mode 100644
index 000000000..832a901e8
--- /dev/null
+++ b/cloud/app/src/style/index.css
@@ -0,0 +1,8 @@
+@import "./token/color.css";
+@import "./token/font.css";
+@import "./token/space.css";
+
+@import "./component/button.css";
+
+@import "./reset.css";
+@import "./base.css";
diff --git a/cloud/app/src/style/reset.css b/cloud/app/src/style/reset.css
new file mode 100644
index 000000000..d331ed724
--- /dev/null
+++ b/cloud/app/src/style/reset.css
@@ -0,0 +1,76 @@
+/* 1. Use a more-intuitive box-sizing model */
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+/* 2. Remove default margin */
+* {
+ margin: 0;
+}
+
+/* 3. Enable keyword animations */
+@media (prefers-reduced-motion: no-preference) {
+ html {
+ interpolate-size: allow-keywords;
+ }
+}
+
+body {
+ /* 4. Add accessible line-height */
+ line-height: 1.5;
+ /* 5. Improve text rendering */
+ -webkit-font-smoothing: antialiased;
+}
+
+/* 6. Improve media defaults */
+img,
+picture,
+video,
+canvas,
+svg {
+ display: block;
+ max-width: 100%;
+}
+
+/* 7. Inherit fonts for form controls */
+input,
+button,
+textarea,
+select {
+ font: inherit;
+}
+
+/* 8. Avoid text overflows */
+p,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ overflow-wrap: break-word;
+}
+
+/* 9. Improve line wrapping */
+p {
+ text-wrap: pretty;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ text-wrap: balance;
+}
+
+/*
+ 10. Create a root stacking context
+*/
+#root,
+#__next {
+ isolation: isolate;
+}
diff --git a/cloud/app/src/style/token/color.css b/cloud/app/src/style/token/color.css
new file mode 100644
index 000000000..5382321e3
--- /dev/null
+++ b/cloud/app/src/style/token/color.css
@@ -0,0 +1,90 @@
+body {
+ --color-white: #ffffff;
+ --color-black: #000000;
+}
+
+[data-color-mode="dark"] {
+ /* OpenCode theme colors */
+ --color-bg: #0c0c0e;
+ --color-bg-surface: #161618;
+ --color-bg-elevated: #1c1c1f;
+
+ --color-text: #ffffff;
+ --color-text-muted: #a1a1a6;
+ --color-text-disabled: #68686f;
+
+ --color-accent: #007aff;
+ --color-accent-hover: #0056b3;
+ --color-accent-active: #004085;
+
+ --color-success: #30d158;
+ --color-warning: #ff9f0a;
+ --color-danger: #ff453a;
+
+ --color-border: #38383a;
+ --color-border-muted: #2c2c2e;
+
+ /* Button colors */
+ --color-primary: var(--color-accent);
+ --color-primary-hover: var(--color-accent-hover);
+ --color-primary-active: var(--color-accent-active);
+ --color-primary-text: #ffffff;
+
+ --color-danger: #ff453a;
+ --color-danger-hover: #d70015;
+ --color-danger-active: #a50011;
+ --color-danger-text: #ffffff;
+
+ --color-warning: #ff9f0a;
+ --color-warning-hover: #cc7f08;
+ --color-warning-active: #995f06;
+ --color-warning-text: #000000;
+
+ /* Surface colors */
+ --color-surface: var(--color-bg-surface);
+ --color-surface-hover: var(--color-bg-elevated);
+ --color-border: var(--color-border);
+}
+
+[data-color-mode="light"] {
+ /* OpenCode light theme colors */
+ --color-bg: #ffffff;
+ --color-bg-surface: #f5f5f7;
+ --color-bg-elevated: #ffffff;
+
+ --color-text: #1d1d1f;
+ --color-text-muted: #6e6e73;
+ --color-text-disabled: #86868b;
+
+ --color-accent: #007aff;
+ --color-accent-hover: #0056b3;
+ --color-accent-active: #004085;
+
+ --color-success: #30d158;
+ --color-warning: #ff9f0a;
+ --color-danger: #ff3b30;
+
+ --color-border: #d2d2d7;
+ --color-border-muted: #e5e5ea;
+
+ /* Button colors */
+ --color-primary: var(--color-accent);
+ --color-primary-hover: var(--color-accent-hover);
+ --color-primary-active: var(--color-accent-active);
+ --color-primary-text: #ffffff;
+
+ --color-danger: #ff3b30;
+ --color-danger-hover: #d70015;
+ --color-danger-active: #a50011;
+ --color-danger-text: #ffffff;
+
+ --color-warning: #ff9f0a;
+ --color-warning-hover: #cc7f08;
+ --color-warning-active: #995f06;
+ --color-warning-text: #000000;
+
+ /* Surface colors */
+ --color-surface: var(--color-bg-surface);
+ --color-surface-hover: var(--color-bg-elevated);
+ --color-border: var(--color-border);
+}
diff --git a/cloud/app/src/style/token/font.css b/cloud/app/src/style/token/font.css
new file mode 100644
index 000000000..1852af5b0
--- /dev/null
+++ b/cloud/app/src/style/token/font.css
@@ -0,0 +1,18 @@
+body {
+ --font-size-2xs: 0.6875rem;
+ --font-size-xs: 0.75rem;
+ --font-size-sm: 0.8125rem;
+ --font-size-md: 0.9375rem;
+ --font-size-lg: 1.125rem;
+ --font-size-xl: 1.25rem;
+ --font-size-2xl: 1.5rem;
+ --font-size-3xl: 1.875rem;
+ --font-size-4xl: 2.25rem;
+ --font-size-5xl: 3rem;
+ --font-size-6xl: 3.75rem;
+ --font-size-7xl: 4.5rem;
+ --font-size-8xl: 6rem;
+ --font-size-9xl: 8rem;
+ --font-mono: IBM Plex Mono;
+ --font-sans: Inter;
+}
diff --git a/cloud/app/src/style/token/space.css b/cloud/app/src/style/token/space.css
new file mode 100644
index 000000000..633c1238c
--- /dev/null
+++ b/cloud/app/src/style/token/space.css
@@ -0,0 +1,41 @@
+body {
+ --space-0: 0;
+ --space-px: 1px;
+ --space-0-5: 0.125rem;
+ --space-1: 0.25rem;
+ --space-1-5: 0.375rem;
+ --space-2: 0.5rem;
+ --space-2-5: 0.625rem;
+ --space-3: 0.75rem;
+ --space-3-5: 0.875rem;
+ --space-4: 1rem;
+ --space-4-5: 1.125rem;
+ --space-5: 1.25rem;
+ --space-6: 1.5rem;
+ --space-7: 1.75rem;
+ --space-8: 2rem;
+ --space-9: 2.25rem;
+ --space-10: 2.5rem;
+ --space-11: 2.75rem;
+ --space-12: 3rem;
+ --space-14: 3.5rem;
+ --space-16: 4rem;
+ --space-17: 4.25rem;
+ --space-18: 4.5rem;
+ --space-19: 4.75rem;
+ --space-20: 5rem;
+ --space-24: 6rem;
+ --space-28: 7rem;
+ --space-32: 8rem;
+ --space-36: 9rem;
+ --space-40: 10rem;
+ --space-44: 11rem;
+ --space-48: 12rem;
+ --space-52: 13rem;
+ --space-56: 14rem;
+ --space-60: 15rem;
+ --space-64: 16rem;
+ --space-72: 18rem;
+ --space-80: 20rem;
+ --space-96: 24rem;
+}
diff --git a/cloud/app/tsconfig.json b/cloud/app/tsconfig.json
new file mode 100644
index 000000000..7d5871a07
--- /dev/null
+++ b/cloud/app/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "allowJs": true,
+ "strict": true,
+ "noEmit": true,
+ "types": ["vinxi/types/client"],
+ "isolatedModules": true,
+ "paths": {
+ "~/*": ["./src/*"]
+ }
+ }
+}
diff --git a/cloud/web/src/ui/style/token/space.css b/cloud/web/src/ui/style/token/space.css
index b1e492f49..4a061d756 100644
--- a/cloud/web/src/ui/style/token/space.css
+++ b/cloud/web/src/ui/style/token/space.css
@@ -20,6 +20,7 @@
--space-12: 3rem;
--space-14: 3.5rem;
--space-16: 4rem;
+ --space-18: 4.5rem;
--space-20: 5rem;
--space-24: 6rem;
--space-28: 7rem;