From f3da73553c45f17e04b1e77cb13eb0fca714d1bd Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 30 May 2025 20:47:56 -0400 Subject: sync --- packages/web/.gitignore | 21 + packages/web/README.md | 54 + packages/web/astro.config.mjs | 69 + packages/web/package.json | 29 + packages/web/public/favicon.svg | 3 + packages/web/public/social-share.png | Bin 0 -> 25480 bytes packages/web/src/assets/lander/check.svg | 2 + packages/web/src/assets/lander/copy.svg | 2 + packages/web/src/assets/logo-dark.svg | 11 + packages/web/src/assets/logo-light.svg | 11 + packages/web/src/components/CodeBlock.tsx | 47 + packages/web/src/components/DiffView.tsx | 73 + packages/web/src/components/Header.astro | 62 + packages/web/src/components/Hero.astro | 11 + packages/web/src/components/Lander.astro | 269 + packages/web/src/components/Share.tsx | 772 +++ packages/web/src/components/diffview.module.css | 80 + packages/web/src/components/icons/custom.tsx | 22 + packages/web/src/components/icons/index.tsx | 6101 ++++++++++++++++++++ packages/web/src/components/share.module.css | 326 ++ packages/web/src/content.config.ts | 7 + packages/web/src/content/docs/docs/cli.mdx | 89 + packages/web/src/content/docs/docs/config.mdx | 88 + packages/web/src/content/docs/docs/index.mdx | 58 + packages/web/src/content/docs/docs/lsp-servers.mdx | 34 + packages/web/src/content/docs/docs/mcp-servers.mdx | 51 + packages/web/src/content/docs/docs/models.mdx | 34 + packages/web/src/content/docs/docs/shortcuts.mdx | 68 + packages/web/src/content/docs/docs/themes.mdx | 75 + packages/web/src/content/docs/index.mdx | 12 + packages/web/src/pages/s/index.astro | 28 + packages/web/src/styles/custom.css | 16 + packages/web/sst-env.d.ts | 9 + packages/web/tsconfig.json | 9 + 34 files changed, 8543 insertions(+) create mode 100644 packages/web/.gitignore create mode 100644 packages/web/README.md create mode 100644 packages/web/astro.config.mjs create mode 100644 packages/web/package.json create mode 100644 packages/web/public/favicon.svg create mode 100644 packages/web/public/social-share.png create mode 100644 packages/web/src/assets/lander/check.svg create mode 100644 packages/web/src/assets/lander/copy.svg create mode 100644 packages/web/src/assets/logo-dark.svg create mode 100644 packages/web/src/assets/logo-light.svg create mode 100644 packages/web/src/components/CodeBlock.tsx create mode 100644 packages/web/src/components/DiffView.tsx create mode 100644 packages/web/src/components/Header.astro create mode 100644 packages/web/src/components/Hero.astro create mode 100644 packages/web/src/components/Lander.astro create mode 100644 packages/web/src/components/Share.tsx create mode 100644 packages/web/src/components/diffview.module.css create mode 100644 packages/web/src/components/icons/custom.tsx create mode 100644 packages/web/src/components/icons/index.tsx create mode 100644 packages/web/src/components/share.module.css create mode 100644 packages/web/src/content.config.ts create mode 100644 packages/web/src/content/docs/docs/cli.mdx create mode 100644 packages/web/src/content/docs/docs/config.mdx create mode 100644 packages/web/src/content/docs/docs/index.mdx create mode 100644 packages/web/src/content/docs/docs/lsp-servers.mdx create mode 100644 packages/web/src/content/docs/docs/mcp-servers.mdx create mode 100644 packages/web/src/content/docs/docs/models.mdx create mode 100644 packages/web/src/content/docs/docs/shortcuts.mdx create mode 100644 packages/web/src/content/docs/docs/themes.mdx create mode 100644 packages/web/src/content/docs/index.mdx create mode 100644 packages/web/src/pages/s/index.astro create mode 100644 packages/web/src/styles/custom.css create mode 100644 packages/web/sst-env.d.ts create mode 100644 packages/web/tsconfig.json (limited to 'packages/web') diff --git a/packages/web/.gitignore b/packages/web/.gitignore new file mode 100644 index 000000000..6240da8b1 --- /dev/null +++ b/packages/web/.gitignore @@ -0,0 +1,21 @@ +# build output +dist/ +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + + +# environment variables +.env +.env.production + +# macOS-specific files +.DS_Store diff --git a/packages/web/README.md b/packages/web/README.md new file mode 100644 index 000000000..f9f6d31c6 --- /dev/null +++ b/packages/web/README.md @@ -0,0 +1,54 @@ +# Starlight Starter Kit: Basics + +[![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build) + +``` +npm create astro@latest -- --template starlight +``` + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics) +[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics) +[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/withastro/starlight&create_from_path=examples/basics) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwithastro%2Fstarlight%2Ftree%2Fmain%2Fexamples%2Fbasics&project-name=my-starlight-docs&repository-name=my-starlight-docs) + +> πŸ§‘β€πŸš€ **Seasoned astronaut?** Delete this file. Have fun! + +## πŸš€ Project Structure + +Inside of your Astro + Starlight project, you'll see the following folders and files: + +``` +. +β”œβ”€β”€ public/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ assets/ +β”‚ β”œβ”€β”€ content/ +β”‚ β”‚ β”œβ”€β”€ docs/ +β”‚ └── content.config.ts +β”œβ”€β”€ astro.config.mjs +β”œβ”€β”€ package.json +└── tsconfig.json +``` + +Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name. + +Images can be added to `src/assets/` and embedded in Markdown with a relative link. + +Static assets, like favicons, can be placed in the `public/` directory. + +## 🧞 Commands + +All commands are run from the root of the project, from a terminal: + +| Command | Action | +| :------------------------ | :----------------------------------------------- | +| `npm install` | Installs dependencies | +| `npm run dev` | Starts local dev server at `localhost:4321` | +| `npm run build` | Build your production site to `./dist/` | +| `npm run preview` | Preview your build locally, before deploying | +| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | +| `npm run astro -- --help` | Get help using the Astro CLI | + +## πŸ‘€ Want to learn more? + +Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat). diff --git a/packages/web/astro.config.mjs b/packages/web/astro.config.mjs new file mode 100644 index 000000000..bfe18acd8 --- /dev/null +++ b/packages/web/astro.config.mjs @@ -0,0 +1,69 @@ +// @ts-check +import { defineConfig } from "astro/config"; +import starlight from "@astrojs/starlight"; +import solidJs from "@astrojs/solid-js"; +import theme from "toolbeam-docs-theme"; +import { rehypeHeadingIds } from "@astrojs/markdown-remark"; +import rehypeAutolinkHeadings from "rehype-autolink-headings"; + +const discord = "https://discord.gg/sst"; +const github = "https://github.com/sst/opencode"; + +// https://astro.build/config +export default defineConfig({ + devToolbar: { + enabled: false, + }, + markdown: { + rehypePlugins: [ + rehypeHeadingIds, + [rehypeAutolinkHeadings, { behavior: "wrap" }], + ], + }, + integrations: [ + solidJs(), + starlight({ + title: "OpenCode", + expressiveCode: { themes: ["github-light", "github-dark"] }, + social: [ + { icon: "discord", label: "Discord", href: discord }, + { icon: "github", label: "GitHub", href: github }, + ], + editLink: { + baseUrl: `${github}/edit/master/www/`, + }, + markdown: { + headingLinks: false, + }, + customCss: [ + "./src/styles/custom.css", + ], + logo: { + light: "./src/assets/logo-light.svg", + dark: "./src/assets/logo-dark.svg", + replacesTitle: true, + }, + sidebar: [ + "docs", + "docs/cli", + "docs/config", + "docs/models", + "docs/themes", + "docs/shortcuts", + "docs/lsp-servers", + "docs/mcp-servers", + ], + components: { + Hero: "./src/components/Hero.astro", + Header: "./src/components/Header.astro", + }, + plugins: [theme({ + // Optionally, add your own header links + headerLinks: [ + { name: "Home", url: "/" }, + { name: "Docs", url: "/docs/" }, + ], + })], + }), + ], +}); diff --git a/packages/web/package.json b/packages/web/package.json new file mode 100644 index 000000000..33b3dc4d2 --- /dev/null +++ b/packages/web/package.json @@ -0,0 +1,29 @@ +{ + "name": "@opencode/web", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "@astrojs/markdown-remark": "^6.3.1", + "@astrojs/solid-js": "^5.1.0", + "@astrojs/starlight": "^0.34.3", + "@fontsource/ibm-plex-mono": "^5.2.5", + "@shikijs/transformers": "^3.4.2", + "@types/luxon": "^3.6.2", + "ai": "^5.0.0-alpha.2", + "astro": "^5.7.13", + "diff": "^8.0.2", + "luxon": "^3.6.1", + "rehype-autolink-headings": "^7.1.0", + "sharp": "^0.32.5", + "shiki": "^3.4.2", + "solid-js": "^1.9.7", + "toolbeam-docs-theme": "^0.2.4" + } +} diff --git a/packages/web/public/favicon.svg b/packages/web/public/favicon.svg new file mode 100644 index 000000000..8b011db3f --- /dev/null +++ b/packages/web/public/favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/web/public/social-share.png b/packages/web/public/social-share.png new file mode 100644 index 000000000..23905086a Binary files /dev/null and b/packages/web/public/social-share.png differ diff --git a/packages/web/src/assets/lander/check.svg b/packages/web/src/assets/lander/check.svg new file mode 100644 index 000000000..22de6f2a8 --- /dev/null +++ b/packages/web/src/assets/lander/check.svg @@ -0,0 +1,2 @@ + + diff --git a/packages/web/src/assets/lander/copy.svg b/packages/web/src/assets/lander/copy.svg new file mode 100644 index 000000000..f1baac30a --- /dev/null +++ b/packages/web/src/assets/lander/copy.svg @@ -0,0 +1,2 @@ + + diff --git a/packages/web/src/assets/logo-dark.svg b/packages/web/src/assets/logo-dark.svg new file mode 100644 index 000000000..8fd212081 --- /dev/null +++ b/packages/web/src/assets/logo-dark.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/web/src/assets/logo-light.svg b/packages/web/src/assets/logo-light.svg new file mode 100644 index 000000000..0a9007e1a --- /dev/null +++ b/packages/web/src/assets/logo-light.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/web/src/components/CodeBlock.tsx b/packages/web/src/components/CodeBlock.tsx new file mode 100644 index 000000000..17559ece1 --- /dev/null +++ b/packages/web/src/components/CodeBlock.tsx @@ -0,0 +1,47 @@ +import { + type JSX, + onCleanup, + splitProps, + createEffect, + createResource, +} from "solid-js" +import { codeToHtml } from "shiki" +import { transformerNotationDiff } from '@shikijs/transformers' + +interface CodeBlockProps extends JSX.HTMLAttributes { + code: string + lang?: string +} +function CodeBlock(props: CodeBlockProps) { + const [local, rest] = splitProps(props, ["code", "lang"]) + let containerRef!: HTMLDivElement + + const [html] = createResource(async () => { + return (await codeToHtml(local.code, { + lang: local.lang || "text", + themes: { + light: 'github-light', + dark: 'github-dark', + }, + transformers: [ + transformerNotationDiff(), + ], + })) as string + }) + + onCleanup(() => { + if (containerRef) containerRef.innerHTML = "" + }) + + createEffect(() => { + if (html() && containerRef) { + containerRef.innerHTML = html() as string + } + }) + + return ( +
+ ) +} + +export default CodeBlock diff --git a/packages/web/src/components/DiffView.tsx b/packages/web/src/components/DiffView.tsx new file mode 100644 index 000000000..44feef140 --- /dev/null +++ b/packages/web/src/components/DiffView.tsx @@ -0,0 +1,73 @@ +import { type Component, createSignal, onMount } from "solid-js" +import { diffLines } from "diff" +import CodeBlock from "./CodeBlock" +import styles from "./diffview.module.css" + +type DiffRow = { + left: string + right: string + type: "added" | "removed" | "unchanged" +} + +interface DiffViewProps { + oldCode: string + newCode: string + lang?: string + class?: string +} + +const DiffView: Component = (props) => { + const [rows, setRows] = createSignal([]) + + onMount(() => { + const chunks = diffLines(props.oldCode, props.newCode) + const diffRows: DiffRow[] = [] + + for (const chunk of chunks) { + const lines = chunk.value.split(/\r?\n/) + if (lines.at(-1) === "") lines.pop() + + for (const line of lines) { + diffRows.push({ + left: chunk.removed ? line : chunk.added ? "" : line, + right: chunk.added ? line : chunk.removed ? "" : line, + type: chunk.added + ? "added" + : chunk.removed + ? "removed" + : "unchanged", + }) + } + } + + setRows(diffRows) + }) + + return ( +
+
+ {rows().map((r) => ( + + ))} +
+ +
+ {rows().map((r) => ( + + ))} +
+
+ ) +} + +export default DiffView diff --git a/packages/web/src/components/Header.astro b/packages/web/src/components/Header.astro new file mode 100644 index 000000000..a45899ff8 --- /dev/null +++ b/packages/web/src/components/Header.astro @@ -0,0 +1,62 @@ +--- +import config from 'virtual:starlight/user-config'; +import { Icon } from '@astrojs/starlight/components'; +import { HeaderLinks } from 'toolbeam-docs-theme/components'; +import Default from 'toolbeam-docs-theme/overrides/Header.astro'; +import SiteTitle from '@astrojs/starlight/components/SiteTitle.astro'; + +const path = Astro.url.pathname; + +const links = config.social || []; +--- + +{ path.startsWith("/share") + ?
+
+ +
+
+ +
+
+ : +} + + + + diff --git a/packages/web/src/components/Hero.astro b/packages/web/src/components/Hero.astro new file mode 100644 index 000000000..f80f85266 --- /dev/null +++ b/packages/web/src/components/Hero.astro @@ -0,0 +1,11 @@ +--- +import Default from '@astrojs/starlight/components/Hero.astro'; +import Lander from './Lander.astro'; + +const { slug } = Astro.locals.starlightRoute.entry; +--- + +{ slug === "" + ? + : +} diff --git a/packages/web/src/components/Lander.astro b/packages/web/src/components/Lander.astro new file mode 100644 index 000000000..d27358f8f --- /dev/null +++ b/packages/web/src/components/Lander.astro @@ -0,0 +1,269 @@ +--- +import { Image } from 'astro:assets'; +import config from "virtual:starlight/user-config"; +import type { Props } from '@astrojs/starlight/props'; + +import CopyIcon from "../assets/lander/copy.svg"; +import CheckIcon from "../assets/lander/check.svg"; + +const { data } = Astro.locals.starlightRoute.entry; +const { title = data.title, tagline, image, actions = [] } = data.hero || {}; + +const imageAttrs = { + loading: 'eager' as const, + decoding: 'async' as const, + width: 400, + alt: image?.alt || '', +}; + +const github = config.social.filter(s => s.icon === 'github')[0]; + +const command = "npm i -g"; +const pkg = "opencode"; + +let darkImage: ImageMetadata | undefined; +let lightImage: ImageMetadata | undefined; +let rawHtml: string | undefined; +if (image) { + if ('file' in image) { + darkImage = image.file; + } else if ('dark' in image) { + darkImage = image.dark; + lightImage = image.light; + } else { + rawHtml = image.html; + } +} +--- +
+
+ +

The AI coding agent built for the terminal.

+
+ +
+ +
+ +
+ +
+ +
+
    +
  • Native TUI: A native terminal UI for a smoother, snappier experience.
  • +
  • LSP enabled: Loads the right LSPs for your codebase. Helps the LLM make fewer mistakes.
  • +
  • Multi-session: Start multiple conversations in a project to have agents working in parallel.
  • +
  • Use any model: Supports all the models from OpenAI, Anthropic, Google, OpenRouter, and more.
  • +
  • Change tracking: View the file changes from the current conversation in the sidebar.
  • +
  • Edit with Vim: Use Vim as an external editor to compose longer messages.
  • +
+
+ + +
+ + + + + + diff --git a/packages/web/src/components/Share.tsx b/packages/web/src/components/Share.tsx new file mode 100644 index 000000000..ac75a3cf7 --- /dev/null +++ b/packages/web/src/components/Share.tsx @@ -0,0 +1,772 @@ +import { type JSX } from "solid-js" +import { + For, + Show, + Match, + Switch, + onMount, + onCleanup, + splitProps, + createMemo, + createEffect, + createSignal, +} from "solid-js" +import { DateTime } from "luxon" +import { + IconOpenAI, + IconGemini, + IconAnthropic, +} from "./icons/custom" +import { + IconCpuChip, + IconSparkles, + IconUserCircle, + IconChevronDown, + IconChevronRight, + IconPencilSquare, + IconWrenchScrewdriver, +} from "./icons" +import DiffView from "./DiffView" +import styles from "./share.module.css" +import { type UIMessage } from "ai" +import { createStore, reconcile } from "solid-js/store" + +type Status = "disconnected" | "connecting" | "connected" | "error" | "reconnecting" + + +type SessionMessage = UIMessage<{ + time: { + created: number + completed?: number + } + assistant?: { + modelID: string; + providerID: string; + cost: number; + tokens: { + input: number; + output: number; + reasoning: number; + }; + }; + sessionID: string + tool: Record + time: { + start: number + end: number + } + }> +}> + +type SessionInfo = { + title: string + cost?: number +} + +function getFileType(path: string) { + return path.split('.').pop() +} + +// Converts `{a:{b:{c:1}}` to `[['a.b.c', 1]]` +function flattenToolArgs(obj: any, prefix: string = ""): Array<[string, any]> { + const entries: Array<[string, any]> = []; + + for (const [key, value] of Object.entries(obj)) { + const path = prefix ? `${prefix}.${key}` : key; + + if ( + value !== null && + typeof value === "object" && + !Array.isArray(value) + ) { + entries.push(...flattenToolArgs(value, path)); + } + else { + entries.push([path, value]); + } + } + + return entries; +} + +function getStatusText(status: [Status, string?]): string { + switch (status[0]) { + case "connected": return "Connected" + case "connecting": return "Connecting..." + case "disconnected": return "Disconnected" + case "reconnecting": return "Reconnecting..." + case "error": return status[1] || "Error" + default: return "Unknown" + } +} + +function ProviderIcon(props: { provider: string, size?: number }) { + const size = props.size || 16 + return ( + + }> + + + + + + + + + + + ) +} + +interface ResultsButtonProps extends JSX.HTMLAttributes { + results: boolean +} +function ResultsButton(props: ResultsButtonProps) { + const [local, rest] = splitProps(props, ["results"]) + return ( + + ) +} + +interface TextPartProps extends JSX.HTMLAttributes { + text: string + expand?: boolean + highlight?: boolean +} +function TextPart(props: TextPartProps) { + const [local, rest] = splitProps(props, ["text", "expand", "highlight"]) + const [expanded, setExpanded] = createSignal(false) + const [overflowed, setOverflowed] = createSignal(false) + let preEl: HTMLPreElement | undefined + + function checkOverflow() { + if (preEl && !local.expand) { + setOverflowed(preEl.scrollHeight > preEl.clientHeight + 1) + } + } + + onMount(() => { + checkOverflow() + window.addEventListener("resize", checkOverflow) + }) + + createEffect(() => { + local.text + setTimeout(checkOverflow, 0) + }) + + onCleanup(() => { + window.removeEventListener("resize", checkOverflow) + }) + + return ( +
+
 (preEl = el)}>{local.text}
+ {overflowed() && + + } +
+ ) +} + +function PartFooter(props: { time: number }) { + return ( + + {DateTime.fromMillis(props.time).toLocaleString(DateTime.TIME_WITH_SECONDS)} + + ) +} + +export default function Share(props: { api: string }) { + let params = new URLSearchParams(document.location.search) + const id = params.get("id") + + const [store, setStore] = createStore<{ + info?: SessionInfo + messages: Record + }>({ + messages: {}, + }) + const messages = createMemo(() => Object.values(store.messages).toSorted((a, b) => a.id?.localeCompare(b.id))) + const [connectionStatus, setConnectionStatus] = createSignal<[Status, string?]>(["disconnected", "Disconnected"]) + + onMount(() => { + const apiUrl = props.api + + if (!id) { + setConnectionStatus(["error", "id not found"]) + return + } + + if (!apiUrl) { + console.error("API URL not found in environment variables") + setConnectionStatus(["error", "API URL not found"]) + return + } + + let reconnectTimer: number | undefined + let socket: WebSocket | null = null + + // Function to create and set up WebSocket with auto-reconnect + const setupWebSocket = () => { + // Close any existing connection + if (socket) { + socket.close() + } + + setConnectionStatus(["connecting"]) + + // Always use secure WebSocket protocol (wss) + const wsBaseUrl = apiUrl.replace(/^https?:\/\//, "wss://") + const wsUrl = `${wsBaseUrl}/share_poll?id=${id}` + console.log("Connecting to WebSocket URL:", wsUrl) + + // Create WebSocket connection + socket = new WebSocket(wsUrl) + + // Handle connection opening + socket.onopen = () => { + setConnectionStatus(["connected"]) + console.log("WebSocket connection established") + } + + // Handle incoming messages + socket.onmessage = (event) => { + console.log("WebSocket message received") + try { + const data = JSON.parse(event.data) + const [root, type, ...splits] = data.key.split("/") + if (root !== "session") return + if (type === "info") { + setStore("info", reconcile(data.content)) + return + } + if (type === "message") { + const [, messageID] = splits + setStore("messages", messageID, reconcile(data.content)) + } + } catch (error) { + console.error("Error parsing WebSocket message:", error) + } + } + + // Handle errors + socket.onerror = (error) => { + console.error("WebSocket error:", error) + setConnectionStatus(["error", "Connection failed"]) + } + + // Handle connection close and reconnection + socket.onclose = (event) => { + console.log(`WebSocket closed: ${event.code} ${event.reason}`) + setConnectionStatus(["reconnecting"]) + + // Try to reconnect after 2 seconds + clearTimeout(reconnectTimer) + reconnectTimer = window.setTimeout( + setupWebSocket, + 2000, + ) as unknown as number + } + } + + // Initial connection + setupWebSocket() + + // Clean up on component unmount + onCleanup(() => { + console.log("Cleaning up WebSocket connection") + if (socket) { + socket.close() + } + clearTimeout(reconnectTimer) + }) + }) + + const models = createMemo(() => { + const result: string[][] = [] + for (const msg of messages()) { + if (msg.role === "assistant" && msg.metadata?.assistant) { + result.push([msg.metadata.assistant.providerID, msg.metadata.assistant.modelID]) + } + } + return result + }) + + const metrics = createMemo(() => { + const result = { + cost: 0, + tokens: { + input: 0, + output: 0, + reasoning: 0, + } + } + for (const msg of messages()) { + const assistant = msg.metadata?.assistant + if (!assistant) continue + result.cost += assistant.cost + result.tokens.input += assistant.tokens.input + result.tokens.output += assistant.tokens.output + result.tokens.reasoning += assistant.tokens.reasoning + } + return result + }) + + return ( +
+
+
+

{store.info?.title}

+

+ + {getStatusText(connectionStatus())} +

+
+
+
    +
  • + Cost + {metrics().cost !== undefined ? + ${metrics().cost.toFixed(2)} + : + + } +
  • +
  • + Input Tokens + {metrics().tokens.input ? + {metrics().tokens.input} + : + + } +
  • +
  • + Output Tokens + {metrics().tokens.output ? + {metrics().tokens.output} + : + + } +
  • +
  • + Reasoning Tokens + {metrics().tokens.reasoning ? + {metrics().tokens.reasoning} + : + + } +
  • +
+
    + {models().length > 0 ? + + {([provider, model]) => ( +
  • +
    + +
    + {model} +
  • + )} +
    + : +
  • + Models + +
  • + } +
+
+ {messages().length > 0 && messages()[0].metadata?.time.created ? + + {DateTime.fromMillis( + messages()[0].metadata?.time.created || 0 + ).toLocaleString(DateTime.DATE_MED)} + + : + Started at — + } +
+
+
+ +
+ 0} + fallback={

Waiting for messages...

} + > +
+ + {(msg, msgIndex) => ( + + {(part, partIndex) => { + if (part.type === "step-start" && (partIndex() > 0 || !msg.metadata?.assistant)) return null + + const [results, showResults] = createSignal(false) + const isLastPart = createMemo(() => + (messages().length === msgIndex() + 1) + && (msg.parts.length === partIndex() + 1) + ) + const time = msg.metadata?.time.completed + || msg.metadata?.time.created + || 0 + return ( + + { /* User text */} + + {part => +
+
+
+ +
+
+
+
+ + +
+
+ } +
+ { /* AI text */} + + {part => +
+
+
+
+
+
+ + +
+
+ } +
+ { /* AI model */} + + {assistant => +
+
+
+ +
+
+
+
+
+ + {assistant().providerID} + + + {assistant().modelID} + +
+
+
+ } +
+ { /* System text */} + + {part => +
+
+
+ +
+
+
+
+
+ + System + + +
+ +
+
+ } +
+ { /* Edit tool */} + + {part => { + const args = part().toolInvocation.args + const filePath = args.filePath + return ( +
+
+
+ +
+
+
+
+
+ + Edit {filePath} + +
+ +
+
+ +
+
+ ) + }} +
+ { /* Tool call */} + + {part => +
+
+
+ +
+
+
+
+
+ + {part().toolInvocation.toolName} + +
+ + {([name, value]) => + <> +
+
{name}
+
{value}
+ + } +
+
+ + +
+ showResults(e => !e)} + /> + + + +
+
+ + + +
+
+ +
+
+ } +
+ { /* Fallback */} + +
+
+
+ + }> + + + + + + + + + + +
+
+
+
+
+ + {part.type} + + +
+ +
+
+
+
+ ) + }} +
+ )} +
+
+
+
+ +
+
+ 0} + fallback={

Waiting for messages...

} + > +
    + + {(msg) => ( +
  • +
    + Key: {msg.id} +
    +
    {JSON.stringify(msg, null, 2)}
    +
  • + )} +
    +
+
+
+
+
+ ) +} diff --git a/packages/web/src/components/diffview.module.css b/packages/web/src/components/diffview.module.css new file mode 100644 index 000000000..1a0e6c523 --- /dev/null +++ b/packages/web/src/components/diffview.module.css @@ -0,0 +1,80 @@ +.diff { + display: grid; + grid-template-columns: 1fr 1fr; + border: 1px solid var(--sl-color-divider); + background-color: var(--sl-color-bg-surface); + border-radius: 0.25rem; +} + +.column { + display: flex; + flex-direction: column; + overflow-x: auto; + min-width: 0; + align-items: flex-start; + + &:first-child { + border-right: 1px solid var(--sl-color-divider); + } + + & > [data-section="cell"]:first-child { + padding-top: 0.5rem; + } + & > [data-section="cell"]:last-child { + padding-bottom: 0.5rem; + } +} + +[data-section="cell"] { + position: relative; + flex: none; + width: max-content; + padding: 0.1875rem 0.5rem 0.1875rem 1.8ch; + margin: 0; + + pre { + background-color: var(--sl-color-bg-surface) !important; + white-space: pre; + + code > span:empty::before { + content: "\00a0"; + white-space: pre; + display: inline-block; + width: 0; + } + } +} + +[data-diff-type="removed"] { + background-color: var(--sl-color-red-low); + min-width: 100%; + + pre { + background-color: var(--sl-color-red-low) !important; + } + + &::before { + content: "-"; + position: absolute; + left: 0.5ch; + user-select: none; + color: var(--sl-color-red-high); + } +} + +[data-diff-type="added"] { + background-color: var(--sl-color-green-low); + min-width: 100%; + + pre { + background-color: var(--sl-color-green-low) !important; + } + + &::before { + content: "+"; + position: absolute; + left: 0.6ch; + user-select: none; + color: var(--sl-color-green-high); + } +} diff --git a/packages/web/src/components/icons/custom.tsx b/packages/web/src/components/icons/custom.tsx new file mode 100644 index 000000000..f016b83cf --- /dev/null +++ b/packages/web/src/components/icons/custom.tsx @@ -0,0 +1,22 @@ +import { type JSX } from "solid-js" + +// https://icones.js.org/collection/ri?s=openai&icon=ri:openai-fill +export function IconOpenAI(props: JSX.SvgSVGAttributes) { + return ( + + ) +} + +// https://icones.js.org/collection/ri?s=anthropic&icon=ri:anthropic-fill +export function IconAnthropic(props: JSX.SvgSVGAttributes) { + return ( + + ) +} + +// https://icones.js.org/collection/ri?s=gemini&icon=ri:gemini-fill +export function IconGemini(props: JSX.SvgSVGAttributes) { + return ( + + ) +} diff --git a/packages/web/src/components/icons/index.tsx b/packages/web/src/components/icons/index.tsx new file mode 100644 index 000000000..9603925d5 --- /dev/null +++ b/packages/web/src/components/icons/index.tsx @@ -0,0 +1,6101 @@ +import { type JSX } from "solid-js" +// heroicons + +export function IconAcademicCap(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconAdjustmentsHorizontal( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconAdjustmentsVertical( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArchiveBoxArrowDown( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArchiveBoxXMark( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArchiveBox(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowDownCircle( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowDownLeft(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowDownOnSquareStack( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowDownOnSquare( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowDownRight(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowDownTray(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowLeftCircle( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowLeftOnRectangle( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowLeft(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowLongDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowLongLeft(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowLongRight(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowLongUp(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowPathRoundedSquare( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowPath(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowRightCircle( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowRightOnRectangle( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowRight(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowSmallDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowSmallLeft(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowSmallRight( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowSmallUp(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowTopRightOnSquare( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowTrendingDown( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowTrendingUp( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowUpCircle(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowUpLeft(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowUpOnSquareStack( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowUpOnSquare( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowUpRight(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowUpTray(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowUp(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowUturnDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowUturnLeft(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowUturnRight( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowUturnUp(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowsPointingIn( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowsPointingOut( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowsRightLeft( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowsUpDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconAtSymbol(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBackspace(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBackward(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconBanknotes(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBars2(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBars3BottomLeft( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconBars3BottomRight( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconBars3CenterLeft( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconBars3(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBars4(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBarsArrowDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBarsArrowUp(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBattery0(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBattery100(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBattery50(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBeaker(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBellAlert(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBellSlash(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBellSnooze(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBell(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBoltSlash(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBolt(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconBoltSolid(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBookOpen(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBookmarkSlash(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBookmarkSquare(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBookmark(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBriefcase(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconBugAnt(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBuildingLibrary( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconBuildingOffice2( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconBuildingOffice(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBuildingStorefront( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconCake(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCalculator(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCalendarDays(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCalendar(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCamera(props: JSX.SvgSVGAttributes) { + return ( + + + + + + ) +} +export function IconChartBarSquare(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconChartBar(props: JSX.SvgSVGAttributes) { + return ( + + + + + + ) +} +export function IconChartPie(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconChatBubbleBottomCenterText( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconChatBubbleBottomCenter( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconChatBubbleLeftEllipsis( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconChatBubbleLeftRight( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconChatBubbleLeft(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconChatBubbleOvalLeftEllipsis( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconChatBubbleOvalLeft( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconCheckBadge(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCheckCircle(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCheck(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconChevronDoubleDown( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconChevronDoubleLeft( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconChevronDoubleRight( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconChevronDoubleUp( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconChevronDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconChevronLeft(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconChevronRight(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconChevronUpDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconChevronUp(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCircleStack(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconClipboardDocumentCheck( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconClipboardDocumentList( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconClipboardDocument( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconClipboard(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconClock(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCloudArrowDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCloudArrowUp(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCloud(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCodeBracketSquare( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconCodeBracket(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCog6Tooth(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconCog8Tooth(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconCog(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCommandLine(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconComputerDesktop( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconCpuChip(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCreditCard(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCubeTransparent( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconCube(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCurrencyBangladeshi( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconCurrencyDollar(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCurrencyEuro(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCurrencyPound(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCurrencyRupee(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCurrencyYen(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCursorArrowRays( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconCursorArrowRipple( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconDevicePhoneMobile( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconDeviceTablet(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconDocumentArrowDown( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconDocumentArrowUp( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconDocumentChartBar( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconDocumentCheck(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconDocumentDuplicate( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconDocumentMagnifyingGlass( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconDocumentMinus(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconDocumentPlus(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconDocumentText(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconDocument(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconEllipsisHorizontalCircle( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconEllipsisHorizontal( + props: JSX.SvgSVGAttributes +) { + return ( + + + + + + ) +} +export function IconEllipsisVertical( + props: JSX.SvgSVGAttributes +) { + return ( + + + + + + ) +} +export function IconEnvelopeOpen(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconEnvelope(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconEnvelopeSolid(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconExclamationCircle( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconExclamationTriangle( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconEyeDropper(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconEyeSlash(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconEye(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconFaceFrown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconFaceSmile(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconFilm(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconFingerPrint(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconFire(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconFlag(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconFolderArrowDown( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconFolderMinus(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconFolderOpen(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconFolderPlus(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconFolder(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconForward(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconFunnel(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconGif(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconGiftTop(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconGift(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconGlobeAlt(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconGlobeAmericas(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconGlobeAsiaAustralia( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconGlobeEuropeAfrica( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconHandRaised(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconHandThumbDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconHandThumbUp(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconHashtag(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconHeart(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconHomeModern(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconHome(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconIdentification(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconInboxArrowDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconInboxStack(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconInbox(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconInformationCircle( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconKey(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconLanguage(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconLifebuoy(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconLightBulb(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconLink(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconListBullet(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconLockClosed(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconLockOpen(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconMagnifyingGlassCircle( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconMagnifyingGlassMinus( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconMagnifyingGlassPlus( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconMagnifyingGlass( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconMapPin(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconMap(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconMegaphone(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconMicrophone(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconMinusCircle(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconMinusSmall(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconMinus(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconMoon(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconMusicalNote(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconNewspaper(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconNoSymbol(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPaintBrush(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPaperAirplane(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPaperClip(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPauseCircle(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPause(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPencilSquare(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPencil(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPhoneArrowDownLeft( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconPhoneArrowUpRight( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconPhoneXMark(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPhone(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPhoto(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPlayCircle(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconPlayPause(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPlay(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPlusCircle(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPlusSmall(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPlus(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPower(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPresentationChartBar( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconPresentationChartLine( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconPrinter(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPuzzlePiece(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconQrCode(props: JSX.SvgSVGAttributes) { + return ( + + + + + + + + + + + + + + ) +} +export function IconQuestionMarkCircle( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconQueueList(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconRadio(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconReceiptPercent(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconReceiptRefund(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconRectangleGroup(props: JSX.SvgSVGAttributes) { + return ( + + + + + + ) +} +export function IconRectangleStack(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconRocketLaunch(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconRss(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconScale(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconScissors(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconServerStack(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconServer(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconShare(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconShieldCheck(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconShieldExclamation( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconShoppingBag(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconShoppingCart(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconSignalSlash(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconSignal(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconSparkles(props: JSX.SvgSVGAttributes) { + return ( + + + + + + ) +} +export function IconSpeakerWave(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconSpeakerXMark(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconSquare2Stack(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconSquare3Stack3d(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconSquares2x2(props: JSX.SvgSVGAttributes) { + return ( + + + + + + + ) +} +export function IconSquaresPlus(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconStar(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconStopCircle(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconStop(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconSun(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconSwatch(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconTableCells(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconTag(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconTicket(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconTrash(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconTrophy(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconTruck(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconTv(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconUserCircle(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconUserGroup(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconUserMinus(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconUserPlus(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconUser(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconUsers(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconVariable(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconVideoCameraSlash( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconVideoCamera(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconViewColumns(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconViewfinderCircle( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconWallet(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconWifi(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconWindow(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconWrenchScrewdriver( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconWrench(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconXCircle(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconXMark(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +// index +export function IconCommand(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconLetter(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconMultiSelect(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconSettings(props: JSX.SvgSVGAttributes) { + return ( + + + + + + + + + + + + ) +} +export function IconSingleSelect(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} diff --git a/packages/web/src/components/share.module.css b/packages/web/src/components/share.module.css new file mode 100644 index 000000000..5d1dab1bf --- /dev/null +++ b/packages/web/src/components/share.module.css @@ -0,0 +1,326 @@ +.root { + padding-top: 0.5rem; + display: flex; + flex-direction: column; + gap: 2.5rem; + line-height: 1; +} + +[data-element-button-text] { + cursor: pointer; + appearance: none; + background-color: transparent; + border: none; + padding: 0; + color: var(--sl-color-text-secondary); + + &:hover { + color: var(--sl-color-text); + } +} + +[data-element-button-text] { + cursor: pointer; + appearance: none; + background-color: transparent; + border: none; + padding: 0; + color: var(--sl-color-text-secondary); + + &:hover { + color: var(--sl-color-text); + } + + &[data-element-button-more] { + display: flex; + align-items: center; + gap: 0.125rem; + + span[data-button-icon] { + line-height: 1; + opacity: 0.85; + svg { + display: block; + } + } + } +} + +[data-element-label] { + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--sl-color-text-dimmed); +} + +.header { + display: flex; + flex-direction: column; + gap: 0.75rem; + + [data-section="title"] { + display: flex; + align-items: center; + justify-content: space-between; + } + + [data-section="row"] { + display: flex; + flex-direction: column; + gap: 0.375rem; + } + + h1 { + font-size: 1.75rem; + font-weight: 500; + line-height: 1.125; + letter-spacing: -0.05em; + } + p { + flex: 0 0 auto; + display: flex; + gap: 0.375rem; + font-size: 0.75rem; + + span:first-child { + color: var(--sl-color-divider); + + &[data-status="connected"] { color: var(--sl-color-green); } + &[data-status="connecting"] { color: var(--sl-color-orange); } + &[data-status="disconnected"] { color: var(--sl-color-divider); } + &[data-status="reconnecting"] { color: var(--sl-color-orange); } + &[data-status="error"] { color: var(--sl-color-red); } + } + } + + [data-section="stats"] { + list-style-type: none; + padding: 0; + margin: 0; + display: flex; + gap: 1rem; + + li { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; + + span[data-placeholder] { + color: var(--sl-color-text-dimmed); + } + } + } + + [data-section="stats"][data-section-models] { + li { + gap: 0.3125rem; + + [data-stat-model-icon] { + flex: 0 0 auto; + color: var(--sl-color-text-dimmed); + opacity: 0.85; + svg { + display: block; + } + } + + span[data-stat-model] { + color: var(sl-color-text); + } + } + } + + [data-section="date"] { + span { + font-size: 0.875rem; + color: var(--sl-color-text); + + &[data-placeholder] { + color: var(--sl-color-text-dimmed); + } + } + } +} + +.parts { + display: flex; + flex-direction: column; + gap: 0.625rem; + + [data-section="part"] { + display: flex; + gap: 0.625rem; + } + + [data-section="decoration"] { + flex: 0 0 auto; + display: flex; + flex-direction: column; + gap: 0.625rem; + align-items: center; + justify-content: flex-start; + + div:first-child { + flex: 0 0 auto; + width: 18px; + svg { + color: var(--sl-color-text-secondary); + display: block; + } + } + + div:last-child { + width: 3px; + height: 100%; + border-radius: 1px; + background-color: var(--sl-color-hairline); + } + } + + [data-section="content"] { + padding: 0 0 0.375rem; + display: flex; + flex-direction: column; + gap: 1rem; + + [data-part-tool-body] { + display: flex; + flex-direction: column; + gap: 0.375rem; + } + + span[data-part-title] { + line-height: 18px; + font-size: 0.75rem; + + &[data-size="md"] { + font-size: 0.875rem; + } + } + + span[data-part-footer] { + align-self: flex-start; + font-size: 0.75rem; + color: var(--sl-color-text-dimmed); + } + + span[data-part-model] { + line-height: 1.5; + } + + [data-part-tool-args] { + display: inline-grid; + align-items: center; + grid-template-columns: max-content max-content minmax(0, 1fr); + max-width: 100%; + gap: 0.25rem 0.375rem; + + + & > div:nth-child(3n+1) { + width: 8px; + height: 2px; + border-radius: 1px; + background: var(--sl-color-divider); + } + + & > div:nth-child(3n+2), + & > div:nth-child(3n+3) { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 0.75rem; + line-height: 1.5; + } + + & > div:nth-child(3n+3) { + padding-left: 0.125rem; + color: var(--sl-color-text-dimmed); + } + } + + [data-part-tool-result] { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + + button { + font-size: 0.75rem; + } + } + } +} + +[data-element-message-text] { + background-color: var(--sl-color-bg-surface); + padding: 0.5rem calc(0.5rem + 3px); + border-radius: 0.25rem; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 1rem; + + pre { + line-height: 1.5; + font-size: 0.875rem; + white-space: pre-wrap; + overflow-wrap: anywhere; + color: var(--sl-color-text); + } + + &[data-size="sm"] { + pre { + font-size: 0.75rem; + } + } + + &[data-color="dimmed"] { + pre { + color: var(--sl-color-text-dimmed); + } + } + + button { + flex: 0 0 auto; + padding: 2px 0; + font-size: 0.75rem; + } + + &[data-highlight="true"] { + background-color: var(--sl-color-blue-high); + + pre { + color: var(--sl-color-text-invert); + } + + button { + opacity: 0.85; + color: var(--sl-color-text-invert); + + &:hover { + opacity: 1; + } + } + } + + &[data-expanded="true"] { + pre { + display: block; + } + } + &[data-expanded="false"] { + pre { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + overflow: hidden; + } + } +} + +.code-block { + pre { + line-height: 1.25; + font-size: 0.75rem; + } +} diff --git a/packages/web/src/content.config.ts b/packages/web/src/content.config.ts new file mode 100644 index 000000000..d9ee8c9d1 --- /dev/null +++ b/packages/web/src/content.config.ts @@ -0,0 +1,7 @@ +import { defineCollection } from 'astro:content'; +import { docsLoader } from '@astrojs/starlight/loaders'; +import { docsSchema } from '@astrojs/starlight/schema'; + +export const collections = { + docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), +}; diff --git a/packages/web/src/content/docs/docs/cli.mdx b/packages/web/src/content/docs/docs/cli.mdx new file mode 100644 index 000000000..44a56e1fb --- /dev/null +++ b/packages/web/src/content/docs/docs/cli.mdx @@ -0,0 +1,89 @@ +--- +title: CLI +--- + +Once installed you can run the OpenCode CLI. + +```bash +opencode +``` + +Or pass in flags. For example, to start with debug logging: + +```bash +opencode -d +``` + +Or start with a specific working directory. + +```bash +opencode -c /path/to/project +``` + +## Flags + +The OpenCode CLI takes the following flags. + +| Flag | Short | Description | +| -- | -- | -- | +| `--help` | `-h` | Display help | +| `--debug` | `-d` | Enable debug mode | +| `--cwd` | `-c` | Set current working directory | +| `--prompt` | `-p` | Run a single prompt in non-interactive mode | +| `--output-format` | `-f` | Output format for non-interactive mode, `text` or `json` | +| `--quiet` | `-q` | Hide spinner in non-interactive mode | +| `--verbose` | | Display logs to stderr in non-interactive mode | +| `--allowedTools` | | Restrict the agent to only use specified tools | +| `--excludedTools` | | Prevent the agent from using specified tools | + +## Non-interactive + +By default, OpenCode runs in interactive mode. + +But you can also run OpenCode in non-interactive mode by passing a prompt directly as a command-line argument. This is useful for scripting, automation, or when you want a quick answer without launching the full TUI. + +For example, to run a single prompt use the `-p` flag. + +```bash "-p" +opencode -p "Explain the use of context in Go" +``` + +If you want to run without showing the spinner, use `-q`. + +```bash "-q" +opencode -p "Explain the use of context in Go" -q +``` + +In this mode, OpenCode will process your prompt, print the result to standard output, and then exit. All **permissions are auto-approved** for the session. + +#### Tool restrictions + +You can control which tools the AI assistant has access to in non-interactive mode. + +- `--allowedTools` + + A comma-separated list of tools that the agent is allowed to use. Only these tools will be available. + + ```bash "--allowedTools" + opencode -p "Explain the use of context in Go" --allowedTools=view,ls,glob + ``` + +- `--excludedTools` + + Comma-separated list of tools that the agent is not allowed to use. All other tools will be available. + + ```bash "--excludedTools" + opencode -p "Explain the use of context in Go" --excludedTools=bash,edit + ``` + +These flags are mutually exclusive. So you can either use `--allowedTools` or `--excludedTools`, but not both. + +#### Output formats + +In non-interactive mode, you can also set the CLI to return as JSON using `-f`. + +```bash "-f json" +opencode -p "Explain the use of context in Go" -f json +``` + +By default, this is set to `text`, to return plain text. diff --git a/packages/web/src/content/docs/docs/config.mdx b/packages/web/src/content/docs/docs/config.mdx new file mode 100644 index 000000000..288f194c5 --- /dev/null +++ b/packages/web/src/content/docs/docs/config.mdx @@ -0,0 +1,88 @@ +--- +title: Config +--- + +You can configure OpenCode using the OpenCode config. It can be places in: + +- `$HOME/.opencode.json` +- `$XDG_CONFIG_HOME/opencode/.opencode.json` + +Or in the current directory, `./.opencode.json`. + +## OpenCode config + +The config file has the following structure. + +```json title=".opencode.json" +{ + "data": { + "directory": ".opencode" + }, + "providers": { + "openai": { + "apiKey": "your-api-key", + "disabled": false + }, + "anthropic": { + "apiKey": "your-api-key", + "disabled": false + }, + "groq": { + "apiKey": "your-api-key", + "disabled": false + }, + "openrouter": { + "apiKey": "your-api-key", + "disabled": false + } + }, + "agents": { + "primary": { + "model": "claude-3.7-sonnet", + "maxTokens": 5000 + }, + "task": { + "model": "claude-3.7-sonnet", + "maxTokens": 5000 + }, + "title": { + "model": "claude-3.7-sonnet", + "maxTokens": 80 + } + }, + "mcpServers": { + "example": { + "type": "stdio", + "command": "path/to/mcp-server", + "env": [], + "args": [] + } + }, + "lsp": { + "go": { + "disabled": false, + "command": "gopls" + } + }, + "debug": false, + "debugLSP": false +} +``` + +## Environment variables + +For the providers, you can also specify the keys using environment variables. + +| Environment Variable | Models | +| -------------------------- | ----------- | +| `ANTHROPIC_API_KEY` | Claude | +| `OPENAI_API_KEY` | OpenAI | +| `GEMINI_API_KEY` | Google Gemini | +| `GROQ_API_KEY` | Groq | +| `AWS_ACCESS_KEY_ID` | Amazon Bedrock | +| `AWS_SECRET_ACCESS_KEY` | Amazon Bedrock | +| `AWS_REGION` | Amazon Bedrock | +| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI | +| `AZURE_OPENAI_API_KEY` | Azure OpenAI, optional when using Entra ID | +| `AZURE_OPENAI_API_VERSION` | Azure OpenAI | + diff --git a/packages/web/src/content/docs/docs/index.mdx b/packages/web/src/content/docs/docs/index.mdx new file mode 100644 index 000000000..e6f71be19 --- /dev/null +++ b/packages/web/src/content/docs/docs/index.mdx @@ -0,0 +1,58 @@ +--- +title: Intro +--- + +OpenCode is an AI coding agent built natively for the terminal. It features: + +- Native TUI for a smoother, snappier experience +- Uses LSPs to help the LLM make fewer mistakes +- Opening multiple conversations with the same project +- Use of any model through the AI SDK +- Tracks and visualizes all the file changes +- Editing longer messages with Vim + +## Installation + +```bash +npm i -g opencode +``` + +If you don't have NPM installed, you can also install the OpenCode binary through the following. + +#### Using the install script + +```bash +curl -fsSL https://opencode.ai/install | bash +``` + +Or install a specific version. + +```bash +curl -fsSL https://opencode.ai/install | VERSION=0.1.0 bash +``` + +#### Using Homebrew on macOS and Linux + +```bash +brew install sst/tap/opencode +``` + +#### Using AUR in Arch Linux + +With yay. + +```bash +yay -S opencode-bin +``` + +Or with paru. + +```bash +paru -S opencode-bin +``` + +#### Using Go + +```bash +go install github.com/sst/opencode@latest +``` diff --git a/packages/web/src/content/docs/docs/lsp-servers.mdx b/packages/web/src/content/docs/docs/lsp-servers.mdx new file mode 100644 index 000000000..cd259dea7 --- /dev/null +++ b/packages/web/src/content/docs/docs/lsp-servers.mdx @@ -0,0 +1,34 @@ +--- +title: LSP servers +--- + +OpenCode integrates with _Language Server Protocol_, or LSP to improve how the LLM interacts with your codebase. + +LSP servers for different languages give the LLM: + +- **Diagnostics**: These include things like errors and lint warnings. So the LLM can generate code that has fewer mistakes without having to run the code. +- **Quick actions**: The LSP can allow the LLM to better navigate the codebase through features like _go-to-definition_ and _find references_. + +## Auto-detection + +By default, OpenCode will **automatically detect** the languages used in your project and add the right LSP servers. + +## Manual configuration + +You can also manually configure LSP servers by adding them under the `lsp` section in your OpenCode config. + +```json title=".opencode.json" +{ + "lsp": { + "go": { + "disabled": false, + "command": "gopls" + }, + "typescript": { + "disabled": false, + "command": "typescript-language-server", + "args": ["--stdio"] + } + } +} +``` diff --git a/packages/web/src/content/docs/docs/mcp-servers.mdx b/packages/web/src/content/docs/docs/mcp-servers.mdx new file mode 100644 index 000000000..28c6d2ab2 --- /dev/null +++ b/packages/web/src/content/docs/docs/mcp-servers.mdx @@ -0,0 +1,51 @@ +--- +title: MCP servers +--- + +You can add external tools to OpenCode using the _Model Context Protocol_, or MCP. OpenCode supports both: + +- Local servers that use standard input/output, `stdio` +- Remote servers that use server-sent events `sse` + +## Add MCP servers + +You can define MCP servers in your OpenCode config under the `mcpServers` section: + +### Local + +To add a local or `stdio` MCP server. + +```json title=".opencode.json" {4} +{ + "mcpServers": { + "local-example": { + "type": "stdio", + "command": "path/to/mcp-server", + "env": [], + "args": [] + } + } +} +``` + +### Remote + +To add a remote or `sse` MCP server. + +```json title=".opencode.json" {4} +{ + "mcpServers": { + "remote-example": { + "type": "sse", + "url": "https://example.com/mcp", + "headers": { + "Authorization": "Bearer token" + } + } + } +} +``` + +## Usage + +Once added, MCP tools are automatically available to the LLM alongside built-in tools. They follow the same permission model; requiring user approval before execution. diff --git a/packages/web/src/content/docs/docs/models.mdx b/packages/web/src/content/docs/docs/models.mdx new file mode 100644 index 000000000..c40216695 --- /dev/null +++ b/packages/web/src/content/docs/docs/models.mdx @@ -0,0 +1,34 @@ +--- +title: Models +--- + +OpenCode uses the [AI SDK](https://ai-sdk.dev/) to have the support for **all the AI models**. + +Start by setting the [keys for the providers](/docs/config) you want to use in your OpenCode config. + +## Model select + +You can now select the model you want from the menu by hitting `Ctrl+O`. + +## Multiple models + +You can also use specific models for specific tasks. For example, you can use a smaller model to generate the title of the conversation or to run a sub task. + +```json title=".opencode.json" +{ + "agents": { + "primary": { + "model": "gpt-4", + "maxTokens": 5000 + }, + "task": { + "model": "gpt-3.5-turbo", + "maxTokens": 5000 + }, + "title": { + "model": "gpt-3.5-turbo", + "maxTokens": 80 + } + } +} +``` diff --git a/packages/web/src/content/docs/docs/shortcuts.mdx b/packages/web/src/content/docs/docs/shortcuts.mdx new file mode 100644 index 000000000..dd866e0f3 --- /dev/null +++ b/packages/web/src/content/docs/docs/shortcuts.mdx @@ -0,0 +1,68 @@ +--- +title: Keyboard shortcuts +sidebar: + label: Shortcuts +--- + +Below are a list of keyboard shortcuts that OpenCode supports. + +## Global + +| Shortcut | Action | +| -------- | ------------------------------------------------------- | +| `Ctrl+C` | Quit application | +| `Ctrl+?` | Toggle help dialog | +| `?` | Toggle help dialog (when not in editing mode) | +| `Ctrl+L` | View logs | +| `Ctrl+A` | Switch session | +| `Ctrl+K` | Command dialog | +| `Ctrl+O` | Toggle model selection dialog | +| `Esc` | Close current overlay/dialog or return to previous mode | + +## Chat pane + +| Shortcut | Action | +| -------- | --------------------------------------- | +| `Ctrl+N` | Create new session | +| `Ctrl+X` | Cancel current operation/generation | +| `i` | Focus editor (when not in writing mode) | +| `Esc` | Exit writing mode and focus messages | + +## Editor view + +| Shortcut | Action | +| ------------------- | ----------------------------------------- | +| `Ctrl+S` | Send message (when editor is focused) | +| `Enter` or `Ctrl+S` | Send message (when editor is not focused) | +| `Ctrl+E` | Open external editor | +| `Esc` | Blur editor and focus messages | + +## Session dialog + +| Shortcut | Action | +| ---------- | ---------------- | +| `↑` or `k` | Previous session | +| `↓` or `j` | Next session | +| `Enter` | Select session | +| `Esc` | Close dialog | + +## Model dialog + +| Shortcut | Action | +| ---------- | ----------------- | +| `↑` or `k` | Move up | +| `↓` or `j` | Move down | +| `←` or `h` | Previous provider | +| `β†’` or `l` | Next provider | +| `Esc` | Close dialog | + +## Permission dialog + +| Shortcut | Action | +| ----------------------- | ---------------------------- | +| `←` or `left` | Switch options left | +| `β†’` or `right` or `tab` | Switch options right | +| `Enter` or `space` | Confirm selection | +| `a` | Allow permission | +| `A` | Allow permission for session | +| `d` | Deny permission | diff --git a/packages/web/src/content/docs/docs/themes.mdx b/packages/web/src/content/docs/docs/themes.mdx new file mode 100644 index 000000000..e691a22e7 --- /dev/null +++ b/packages/web/src/content/docs/docs/themes.mdx @@ -0,0 +1,75 @@ +--- +title: Themes +--- + +OpenCode supports most common terminal themes and you can create your own custom theme. + +## Built-in themes + +The following predefined themes are available: + +- `opencode` +- `catppuccin` +- `dracula` +- `flexoki` +- `gruvbox` +- `monokai` +- `onedark` +- `tokyonight` +- `tron` +- `custom` + +Where `opencode` is the default theme and `custom` let's you define your own theme. + +## Setting a theme + +You can set your theme in your OpenCode config. + +```json title=".opencode.json" +{ + "tui": { + "theme": "monokai" + } +} +``` + +## Create a theme + +You can create your own custom theme by setting the `theme: custom` and providing color definitions through the `customTheme`. + +```json title=".opencode.json" +{ + "tui": { + "theme": "custom", + "customTheme": { + "primary": "#ffcc00", + "secondary": "#00ccff", + "accent": { "dark": "#aa00ff", "light": "#ddccff" }, + "error": "#ff0000" + } + } +} +``` + +#### Color keys + +You can define any of the following color keys in your `customTheme`. + +| Type | Color keys | +| --- | --- | +| Base colors | `primary`, `secondary`, `accent` | +| Status colors | `error`, `warning`, `success`, `info` | +| Text colors | `text`, `textMuted`, `textEmphasized` | +| Background colors | `background`, `backgroundSecondary`, `backgroundDarker` | +| Border colors | `borderNormal`, `borderFocused`, `borderDim` | +| Diff view colors | `diffAdded`, `diffRemoved`, `diffContext`, etc. | + +You don't need to define all the color keys. Any undefined colors will fall back to the default `opencode` theme colors. + +#### Color definitions + +Color keys can take: + +1. **Hex string**: A single hex color string, like `"#aabbcc"`, that'll be used for both light and dark terminal backgrounds. + +2. **Light and dark colors**: An object with `dark` and `light` hex colors that'll be set based on the terminal's background. diff --git a/packages/web/src/content/docs/index.mdx b/packages/web/src/content/docs/index.mdx new file mode 100644 index 000000000..176520ec5 --- /dev/null +++ b/packages/web/src/content/docs/index.mdx @@ -0,0 +1,12 @@ +--- +title: OpenCode +description: The AI coding agent built for the terminal. +template: splash +hero: + title: The AI coding agent built for the terminal. + tagline: The AI coding agent built for the terminal. + image: + dark: ../../assets/logo-dark.svg + light: ../../assets/logo-light.svg + alt: OpenCode logo +--- diff --git a/packages/web/src/pages/s/index.astro b/packages/web/src/pages/s/index.astro new file mode 100644 index 000000000..b678c0db9 --- /dev/null +++ b/packages/web/src/pages/s/index.astro @@ -0,0 +1,28 @@ +--- +import config from "virtual:starlight/user-config"; + +import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'; +import Share from "../../components/Share.tsx"; + +--- + + + + + + diff --git a/packages/web/src/styles/custom.css b/packages/web/src/styles/custom.css new file mode 100644 index 000000000..9c4c71f00 --- /dev/null +++ b/packages/web/src/styles/custom.css @@ -0,0 +1,16 @@ +:root { + --sl-color-bg-surface: var(--sl-color-bg-nav); + --sl-color-divider: var(--sl-color-gray-5); +} + +@media (prefers-color-scheme: dark) { + .shiki, + .shiki span { + color: var(--shiki-dark) !important; + background-color: var(--shiki-dark-bg) !important; + /* Optional, if you also want font styles */ + font-style: var(--shiki-dark-font-style) !important; + font-weight: var(--shiki-dark-font-weight) !important; + text-decoration: var(--shiki-dark-text-decoration) !important; + } +} diff --git a/packages/web/sst-env.d.ts b/packages/web/sst-env.d.ts new file mode 100644 index 000000000..b6a7e9066 --- /dev/null +++ b/packages/web/sst-env.d.ts @@ -0,0 +1,9 @@ +/* This file is auto-generated by SST. Do not edit. */ +/* tslint:disable */ +/* eslint-disable */ +/* deno-fmt-ignore-file */ + +/// + +import "sst" +export {} \ No newline at end of file diff --git a/packages/web/tsconfig.json b/packages/web/tsconfig.json new file mode 100644 index 000000000..973603872 --- /dev/null +++ b/packages/web/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "astro/tsconfigs/strict", + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"], + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js" + } +} -- cgit v1.2.3