From 66b18959ebc7b699a74ce69d3adfb4c4dcaa5fd1 Mon Sep 17 00:00:00 2001 From: Jay V Date: Mon, 26 May 2025 17:25:06 -0400 Subject: Merging docs and share app --- app/packages/web/src/App.module.css | 33 - app/packages/web/src/app.tsx | 692 --------------------- app/packages/web/src/assets/favicon.ico | Bin 15086 -> 0 bytes app/packages/web/src/assets/lander/check.svg | 2 + app/packages/web/src/assets/lander/copy.svg | 2 + app/packages/web/src/assets/logo-dark.svg | 11 + app/packages/web/src/assets/logo-light.svg | 11 + app/packages/web/src/components/Header.astro | 57 ++ app/packages/web/src/components/Hero.astro | 11 + app/packages/web/src/components/Lander.astro | 269 ++++++++ app/packages/web/src/components/Share.tsx | 691 ++++++++++++++++++++ app/packages/web/src/content.config.ts | 7 + app/packages/web/src/content/docs/docs/cli.mdx | 89 +++ app/packages/web/src/content/docs/docs/config.mdx | 88 +++ app/packages/web/src/content/docs/docs/index.mdx | 58 ++ .../web/src/content/docs/docs/lsp-servers.mdx | 34 + .../web/src/content/docs/docs/mcp-servers.mdx | 51 ++ app/packages/web/src/content/docs/docs/models.mdx | 34 + .../web/src/content/docs/docs/shortcuts.mdx | 68 ++ app/packages/web/src/content/docs/docs/themes.mdx | 75 +++ app/packages/web/src/content/docs/index.mdx | 12 + app/packages/web/src/index.css | 13 - app/packages/web/src/index.tsx | 24 - app/packages/web/src/logo.svg | 1 - app/packages/web/src/pages/share/index.astro | 39 ++ app/packages/web/src/sst-env.d.ts | 10 - 26 files changed, 1609 insertions(+), 773 deletions(-) delete mode 100644 app/packages/web/src/App.module.css delete mode 100644 app/packages/web/src/app.tsx delete mode 100644 app/packages/web/src/assets/favicon.ico create mode 100644 app/packages/web/src/assets/lander/check.svg create mode 100644 app/packages/web/src/assets/lander/copy.svg create mode 100644 app/packages/web/src/assets/logo-dark.svg create mode 100644 app/packages/web/src/assets/logo-light.svg create mode 100644 app/packages/web/src/components/Header.astro create mode 100644 app/packages/web/src/components/Hero.astro create mode 100644 app/packages/web/src/components/Lander.astro create mode 100644 app/packages/web/src/components/Share.tsx create mode 100644 app/packages/web/src/content.config.ts create mode 100644 app/packages/web/src/content/docs/docs/cli.mdx create mode 100644 app/packages/web/src/content/docs/docs/config.mdx create mode 100644 app/packages/web/src/content/docs/docs/index.mdx create mode 100644 app/packages/web/src/content/docs/docs/lsp-servers.mdx create mode 100644 app/packages/web/src/content/docs/docs/mcp-servers.mdx create mode 100644 app/packages/web/src/content/docs/docs/models.mdx create mode 100644 app/packages/web/src/content/docs/docs/shortcuts.mdx create mode 100644 app/packages/web/src/content/docs/docs/themes.mdx create mode 100644 app/packages/web/src/content/docs/index.mdx delete mode 100644 app/packages/web/src/index.css delete mode 100644 app/packages/web/src/index.tsx delete mode 100644 app/packages/web/src/logo.svg create mode 100644 app/packages/web/src/pages/share/index.astro delete mode 100644 app/packages/web/src/sst-env.d.ts (limited to 'app/packages/web/src') diff --git a/app/packages/web/src/App.module.css b/app/packages/web/src/App.module.css deleted file mode 100644 index 48308b24a..000000000 --- a/app/packages/web/src/App.module.css +++ /dev/null @@ -1,33 +0,0 @@ -.App { - text-align: center; -} - -.logo { - animation: logo-spin infinite 20s linear; - height: 40vmin; - pointer-events: none; -} - -.header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.link { - color: #b318f0; -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/app/packages/web/src/app.tsx b/app/packages/web/src/app.tsx deleted file mode 100644 index 369bcfa41..000000000 --- a/app/packages/web/src/app.tsx +++ /dev/null @@ -1,692 +0,0 @@ -import { createSignal, onCleanup, onMount, Show, For } from "solid-js" -import { useParams } from "@solidjs/router" -import { type UIMessage } from "ai" - -type Message = { - key: string - content: string -} - -type SessionInfo = { - tokens?: { - input?: number - output?: number - reasoning?: number - } -} - -export default function App() { - const params = useParams<{ id: string }>() - const [connectionStatus, setConnectionStatus] = createSignal("Disconnected") - const [sessionInfo, setSessionInfo] = createSignal(null) - const [systemMessage, setSystemMessage] = createSignal(null) - const [messages, setMessages] = createSignal([]) - const [expandedSystemMessage, setExpandedSystemMessage] = createSignal(false) - - onMount(() => { - // Get the API URL from environment - const apiUrl = import.meta.env.VITE_API_URL - const shareId = params.id - - console.log("Mounting Share component with ID:", shareId) - console.log("API URL:", apiUrl) - - if (!shareId) { - console.error("Share ID not found in environment variables") - setConnectionStatus("Error: Share 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?shareID=${shareId}` - 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) as Message - - // Check if this is a session info message - if (data.key.startsWith("session/info/")) { - const infoContent = JSON.parse(data.content) as SessionInfo - setSessionInfo(infoContent) - console.log("Session info updated:", infoContent) - return - } - - // Check if it's a system message - const msgContent = JSON.parse(data.content) as UIMessage - if (msgContent.role === "system") { - setSystemMessage(data) - console.log("System message updated:", data) - return - } - - // Non-system messages - setMessages((prev) => { - // Check if message with this key already exists - const existingIndex = prev.findIndex((msg) => msg.key === data.key) - if (existingIndex >= 0) { - // Update existing message - const updated = [...prev] - updated[existingIndex] = data - return updated - } else { - // Add new message - return [...prev, data] - } - }) - } 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("Disconnected, 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) - }) - }) - - return ( -
-

Share: {params.id}

- -
-

WebSocket Connection

-

- Status: {connectionStatus()} -

- -

Live Updates

- - -
-

Session Information

-
-
- Input Tokens:{" "} - {sessionInfo()?.tokens?.input || 0} -
-
- Output Tokens:{" "} - {sessionInfo()?.tokens?.output || 0} -
-
- Reasoning Tokens:{" "} - {sessionInfo()?.tokens?.reasoning || 0} -
-
-
-
- - {/* Display system message as context in the Session Information block */} - -
-

Context

- {(() => { - try { - const parsed = JSON.parse( - systemMessage()?.content || "", - ) as UIMessage - if ( - parsed.parts && - parsed.parts.length > 0 && - parsed.parts[0].type === "text" - ) { - const text = parsed.parts[0].text || "" - const lines = text.split("\n") - const visibleLines = expandedSystemMessage() - ? lines - : lines.slice(0, 5) - const hasMoreLines = lines.length > 5 - - return ( - <> -
- {/* Create a modified version of the text part for the system message */} - {(() => { - // Create a modified part with truncated text - const modifiedPart = { - ...parsed.parts[0], - text: visibleLines.join("\n"), - } - - return ( - <> -
{modifiedPart.text}
- {hasMoreLines && !expandedSystemMessage() && ( -
- {lines.length - 5} more lines... -
- )} - - ) - })()} -
- {hasMoreLines && ( - - )} - - ) - } - } catch (e) { - return
Error parsing system message
- } - - return null - })()} -
-
- -
- 0} - fallback={

Waiting for messages...

} - > -
    - - {(msg) => ( -
  • -
    - Key: {msg.key} -
    - - {(() => { - try { - const parsed = JSON.parse(msg.content) as UIMessage - const createdTime = parsed.metadata?.time?.created - ? new Date( - parsed.metadata.time.created, - ).toLocaleString() - : "Unknown time" - - return ( - <> -
    - Full Content: -
    -                                {JSON.stringify(parsed, null, 2)}
    -                              
    -
    - - {parsed.parts && parsed.parts.length > 0 && ( -
    -
    - - Role: {parsed.role || "Unknown"} - - - {createdTime} - -
    - -
    - part.type !== "step-start", - )} - > - {(part) => { - if (part.type === "text") { - //{ - // "type": "text", - // "text": "Hello! How can I help you today?" - //} - return ( -
    -                                            [{part.type}] {part.text}{" "}
    -                                          
    - ) - } - if (part.type === "reasoning") { - //{ - // "type": "reasoning", - // "text": "The user asked for a weather forecast. I should call the 'getWeather' tool with the location 'San Francisco'.", - // "providerMetadata": { "step_id": "reason_step_1" } - //} - return ( -
    -                                            [{part.type}] {part.text}
    -                                          
    - ) - } - if (part.type === "tool-invocation") { - return ( -
    -
    - -
    -                                                  [{part.type}]
    -                                                
    {" "} - Tool:{" "} - - {part.toolInvocation.toolName} - -
    - {parsed.metadata?.tool?.[ - part.toolInvocation.toolCallId - ]?.time?.start && - parsed.metadata?.tool?.[ - part.toolInvocation.toolCallId - ]?.time?.end && ( - - {( - (new Date( - parsed.metadata?.tool?.[ - part.toolInvocation.toolCallId - ].time.end, - ) - - new Date( - parsed.metadata?.tool?.[ - part.toolInvocation.toolCallId - ].time.start, - )) / - 1000 - ).toFixed(2)} - s - - )} -
    - {(() => { - if ( - part.toolInvocation.state === - "partial-call" - ) { - //{ - // "type": "tool-invocation", - // "toolInvocation": { - // "state": "partial-call", - // "toolCallId": "tool_abc123", - // "toolName": "searchWeb", - // "argsTextDelta": "{\"query\":\"latest AI news" - // } - //} - return ( - <> -
    -                                                      {
    -                                                        part.toolInvocation
    -                                                          .argsTextDelta
    -                                                      }
    -                                                    
    - ... - - ) - } - if ( - part.toolInvocation.state === - "call" - ) { - //{ - // "type": "tool-invocation", - // "toolInvocation": { - // "state": "call", - // "toolCallId": "tool_abc123", - // "toolName": "searchWeb", - // "args": { "query": "latest AI news", "count": 3 } - // } - //} - return ( -
    -                                                    {JSON.stringify(
    -                                                      part.toolInvocation.args,
    -                                                      null,
    -                                                      2,
    -                                                    )}
    -                                                  
    - ) - } - if ( - part.toolInvocation.state === - "result" - ) { - //{ - // "type": "tool-invocation", - // "toolInvocation": { - // "state": "result", - // "toolCallId": "tool_abc123", - // "toolName": "searchWeb", - // "args": { "query": "latest AI news", "count": 3 }, - // "result": [ - // { "title": "AI SDK v5 Announced", "url": "..." }, - // { "title": "New LLM Achieves SOTA", "url": "..." } - // ] - // } - //} - return ( - <> -
    -                                                      {JSON.stringify(
    -                                                        part.toolInvocation
    -                                                          .args,
    -                                                        null,
    -                                                        2,
    -                                                      )}
    -                                                    
    -
    -                                                      {JSON.stringify(
    -                                                        part.toolInvocation
    -                                                          .result,
    -                                                        null,
    -                                                        2,
    -                                                      )}
    -                                                    
    - - ) - } - if ( - part.toolInvocation.state === - "error" - ) { - //{ - // "type": "tool-invocation", - // "toolInvocation": { - // "state": "error", - // "toolCallId": "tool_abc123", - // "toolName": "searchWeb", - // "args": { "query": "latest AI news", "count": 3 }, - // "errorMessage": "API limit exceeded for searchWeb tool." - // } - //} - return ( - <> -
    -                                                      {JSON.stringify(
    -                                                        part.toolInvocation
    -                                                          .args,
    -                                                        null,
    -                                                        2,
    -                                                      )}
    -                                                    
    -
    -                                                      {
    -                                                        part.toolInvocation
    -                                                          .errorMessage
    -                                                      }
    -                                                    
    - - ) - } - })()} -
    - ) - } - if (part.type === "source") { - //{ - // "type": "source", - // "source": { - // "sourceType": "url", - // "id": "doc_xyz789", - // "url": "https://example.com/research-paper.pdf", - // "title": "Groundbreaking AI Research Paper" - // } - //} - return ( -
    -
    - -
    [{part.type}]
    -
    - - Source:{" "} - {part.source.title || - part.source.id} - -
    - {part.source.url && ( - - )} - {part.source.sourceType && ( -
    - Type: {part.source.sourceType} -
    - )} -
    - ) - } - if (part.type === "file") { - //{ - // "type": "file", - // "mediaType": "image/jpeg", - // "filename": "cat_photo.jpg", - // "url": "https://example-files.com/cats/cat_photo.jpg" - //} - const isImage = - part.mediaType?.startsWith("image/") - - return ( -
    -
    - -
    [{part.type}]
    -
    - File: {part.filename} - {part.mediaType} -
    - - {isImage && part.url ? ( -
    - { -
    - ) : ( -
    - {part.url ? ( - - Download: {part.filename} - - ) : ( -
    - File attachment (no URL - available) -
    - )} -
    - )} -
    - ) - } - return null - }} -
    -
    -
    - )} - - ) - } catch (e) { - return ( -
    - Content: -
    -                              {msg.content}
    -                            
    -
    - ) - } - })()} -
  • - )} -
    -
-
-
-
-
- ) -} diff --git a/app/packages/web/src/assets/favicon.ico b/app/packages/web/src/assets/favicon.ico deleted file mode 100644 index b836b2bcc..000000000 Binary files a/app/packages/web/src/assets/favicon.ico and /dev/null differ diff --git a/app/packages/web/src/assets/lander/check.svg b/app/packages/web/src/assets/lander/check.svg new file mode 100644 index 000000000..22de6f2a8 --- /dev/null +++ b/app/packages/web/src/assets/lander/check.svg @@ -0,0 +1,2 @@ + + diff --git a/app/packages/web/src/assets/lander/copy.svg b/app/packages/web/src/assets/lander/copy.svg new file mode 100644 index 000000000..f1baac30a --- /dev/null +++ b/app/packages/web/src/assets/lander/copy.svg @@ -0,0 +1,2 @@ + + diff --git a/app/packages/web/src/assets/logo-dark.svg b/app/packages/web/src/assets/logo-dark.svg new file mode 100644 index 000000000..8fd212081 --- /dev/null +++ b/app/packages/web/src/assets/logo-dark.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/packages/web/src/assets/logo-light.svg b/app/packages/web/src/assets/logo-light.svg new file mode 100644 index 000000000..0a9007e1a --- /dev/null +++ b/app/packages/web/src/assets/logo-light.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/packages/web/src/components/Header.astro b/app/packages/web/src/components/Header.astro new file mode 100644 index 000000000..f027d7274 --- /dev/null +++ b/app/packages/web/src/components/Header.astro @@ -0,0 +1,57 @@ +--- +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/app/packages/web/src/components/Hero.astro b/app/packages/web/src/components/Hero.astro new file mode 100644 index 000000000..f80f85266 --- /dev/null +++ b/app/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/app/packages/web/src/components/Lander.astro b/app/packages/web/src/components/Lander.astro new file mode 100644 index 000000000..d27358f8f --- /dev/null +++ b/app/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/app/packages/web/src/components/Share.tsx b/app/packages/web/src/components/Share.tsx new file mode 100644 index 000000000..906a9ab9c --- /dev/null +++ b/app/packages/web/src/components/Share.tsx @@ -0,0 +1,691 @@ +import { createSignal, onCleanup, onMount, Show, For } from "solid-js" +import { type UIMessage } from "ai" + +type Message = { + key: string + content: string +} + +type SessionInfo = { + tokens?: { + input?: number + output?: number + reasoning?: number + } +} + +export default function Share(props: { api: string }) { + let params = new URLSearchParams(document.location.search) + const shareId = params.get("id") + + const [connectionStatus, setConnectionStatus] = createSignal("Disconnected") + const [sessionInfo, setSessionInfo] = createSignal(null) + const [systemMessage, setSystemMessage] = createSignal(null) + const [messages, setMessages] = createSignal([]) + const [expandedSystemMessage, setExpandedSystemMessage] = createSignal(false) + + onMount(() => { + const apiUrl = props.api + + console.log("Mounting Share component with ID:", shareId) + console.log("API URL:", apiUrl) + + if (!shareId) { + console.error("Share ID not found in environment variables") + setConnectionStatus("Error: Share 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?shareID=${shareId}` + 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) as Message + + // Check if this is a session info message + if (data.key.startsWith("session/info/")) { + const infoContent = JSON.parse(data.content) as SessionInfo + setSessionInfo(infoContent) + console.log("Session info updated:", infoContent) + return + } + + // Check if it's a system message + const msgContent = JSON.parse(data.content) as UIMessage + if (msgContent.role === "system") { + setSystemMessage(data) + console.log("System message updated:", data) + return + } + + // Non-system messages + setMessages((prev) => { + // Check if message with this key already exists + const existingIndex = prev.findIndex((msg) => msg.key === data.key) + if (existingIndex >= 0) { + // Update existing message + const updated = [...prev] + updated[existingIndex] = data + return updated + } else { + // Add new message + return [...prev, data] + } + }) + } 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("Disconnected, 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) + }) + }) + + return ( +
+

Share: {shareId}

+ +
+

WebSocket Connection

+

+ Status: {connectionStatus()} +

+ +

Live Updates

+ + +
+

Session Information

+
+
+ Input Tokens:{" "} + {sessionInfo()?.tokens?.input || 0} +
+
+ Output Tokens:{" "} + {sessionInfo()?.tokens?.output || 0} +
+
+ Reasoning Tokens:{" "} + {sessionInfo()?.tokens?.reasoning || 0} +
+
+
+
+ + {/* Display system message as context in the Session Information block */} + +
+

Context

+ {(() => { + try { + const parsed = JSON.parse( + systemMessage()?.content || "", + ) as UIMessage + if ( + parsed.parts && + parsed.parts.length > 0 && + parsed.parts[0].type === "text" + ) { + const text = parsed.parts[0].text || "" + const lines = text.split("\n") + const visibleLines = expandedSystemMessage() + ? lines + : lines.slice(0, 5) + const hasMoreLines = lines.length > 5 + + return ( + <> +
+ {/* Create a modified version of the text part for the system message */} + {(() => { + // Create a modified part with truncated text + const modifiedPart = { + ...parsed.parts[0], + text: visibleLines.join("\n"), + } + + return ( + <> +
{modifiedPart.text}
+ {hasMoreLines && !expandedSystemMessage() && ( +
+ {lines.length - 5} more lines... +
+ )} + + ) + })()} +
+ {hasMoreLines && ( + + )} + + ) + } + } catch (e) { + return
Error parsing system message
+ } + + return null + })()} +
+
+ +
+ 0} + fallback={

Waiting for messages...

} + > +
    + + {(msg) => ( +
  • +
    + Key: {msg.key} +
    + + {(() => { + try { + const parsed = JSON.parse(msg.content) as UIMessage + const createdTime = parsed.metadata?.time?.created + ? new Date( + parsed.metadata.time.created, + ).toLocaleString() + : "Unknown time" + + return ( + <> +
    + Full Content: +
    +                                {JSON.stringify(parsed, null, 2)}
    +                              
    +
    + + {parsed.parts && parsed.parts.length > 0 && ( +
    +
    + + Role: {parsed.role || "Unknown"} + + + {createdTime} + +
    + +
    + part.type !== "step-start", + )} + > + {(part) => { + if (part.type === "text") { + //{ + // "type": "text", + // "text": "Hello! How can I help you today?" + //} + return ( +
    +                                            [{part.type}] {part.text}{" "}
    +                                          
    + ) + } + if (part.type === "reasoning") { + //{ + // "type": "reasoning", + // "text": "The user asked for a weather forecast. I should call the 'getWeather' tool with the location 'San Francisco'.", + // "providerMetadata": { "step_id": "reason_step_1" } + //} + return ( +
    +                                            [{part.type}] {part.text}
    +                                          
    + ) + } + if (part.type === "tool-invocation") { + return ( +
    +
    + +
    +                                                  [{part.type}]
    +                                                
    {" "} + Tool:{" "} + + {part.toolInvocation.toolName} + +
    + {parsed.metadata?.tool?.[ + part.toolInvocation.toolCallId + ]?.time?.start && + parsed.metadata?.tool?.[ + part.toolInvocation.toolCallId + ]?.time?.end && ( + + {( + (new Date( + parsed.metadata?.tool?.[ + part.toolInvocation.toolCallId + ].time.end, + ) - + new Date( + parsed.metadata?.tool?.[ + part.toolInvocation.toolCallId + ].time.start, + )) / + 1000 + ).toFixed(2)} + s + + )} +
    + {(() => { + if ( + part.toolInvocation.state === + "partial-call" + ) { + //{ + // "type": "tool-invocation", + // "toolInvocation": { + // "state": "partial-call", + // "toolCallId": "tool_abc123", + // "toolName": "searchWeb", + // "argsTextDelta": "{\"query\":\"latest AI news" + // } + //} + return ( + <> +
    +                                                      {
    +                                                        part.toolInvocation
    +                                                          .argsTextDelta
    +                                                      }
    +                                                    
    + ... + + ) + } + if ( + part.toolInvocation.state === + "call" + ) { + //{ + // "type": "tool-invocation", + // "toolInvocation": { + // "state": "call", + // "toolCallId": "tool_abc123", + // "toolName": "searchWeb", + // "args": { "query": "latest AI news", "count": 3 } + // } + //} + return ( +
    +                                                    {JSON.stringify(
    +                                                      part.toolInvocation.args,
    +                                                      null,
    +                                                      2,
    +                                                    )}
    +                                                  
    + ) + } + if ( + part.toolInvocation.state === + "result" + ) { + //{ + // "type": "tool-invocation", + // "toolInvocation": { + // "state": "result", + // "toolCallId": "tool_abc123", + // "toolName": "searchWeb", + // "args": { "query": "latest AI news", "count": 3 }, + // "result": [ + // { "title": "AI SDK v5 Announced", "url": "..." }, + // { "title": "New LLM Achieves SOTA", "url": "..." } + // ] + // } + //} + return ( + <> +
    +                                                      {JSON.stringify(
    +                                                        part.toolInvocation
    +                                                          .args,
    +                                                        null,
    +                                                        2,
    +                                                      )}
    +                                                    
    +
    +                                                      {JSON.stringify(
    +                                                        part.toolInvocation
    +                                                          .result,
    +                                                        null,
    +                                                        2,
    +                                                      )}
    +                                                    
    + + ) + } + if ( + part.toolInvocation.state === + "error" + ) { + //{ + // "type": "tool-invocation", + // "toolInvocation": { + // "state": "error", + // "toolCallId": "tool_abc123", + // "toolName": "searchWeb", + // "args": { "query": "latest AI news", "count": 3 }, + // "errorMessage": "API limit exceeded for searchWeb tool." + // } + //} + return ( + <> +
    +                                                      {JSON.stringify(
    +                                                        part.toolInvocation
    +                                                          .args,
    +                                                        null,
    +                                                        2,
    +                                                      )}
    +                                                    
    +
    +                                                      {
    +                                                        part.toolInvocation
    +                                                          .errorMessage
    +                                                      }
    +                                                    
    + + ) + } + })()} +
    + ) + } + if (part.type === "source") { + //{ + // "type": "source", + // "source": { + // "sourceType": "url", + // "id": "doc_xyz789", + // "url": "https://example.com/research-paper.pdf", + // "title": "Groundbreaking AI Research Paper" + // } + //} + return ( +
    +
    + +
    [{part.type}]
    +
    + + Source:{" "} + {part.source.title || + part.source.id} + +
    + {part.source.url && ( + + )} + {part.source.sourceType && ( +
    + Type: {part.source.sourceType} +
    + )} +
    + ) + } + if (part.type === "file") { + //{ + // "type": "file", + // "mediaType": "image/jpeg", + // "filename": "cat_photo.jpg", + // "url": "https://example-files.com/cats/cat_photo.jpg" + //} + const isImage = + part.mediaType?.startsWith("image/") + + return ( +
    +
    + +
    [{part.type}]
    +
    + File: {part.filename} + {part.mediaType} +
    + + {isImage && part.url ? ( +
    + { +
    + ) : ( +
    + {part.url ? ( + + Download: {part.filename} + + ) : ( +
    + File attachment (no URL + available) +
    + )} +
    + )} +
    + ) + } + return null + }} +
    +
    +
    + )} + + ) + } catch (e) { + return ( +
    + Content: +
    +                              {msg.content}
    +                            
    +
    + ) + } + })()} +
  • + )} +
    +
+
+
+
+
+ ) +} diff --git a/app/packages/web/src/content.config.ts b/app/packages/web/src/content.config.ts new file mode 100644 index 000000000..d9ee8c9d1 --- /dev/null +++ b/app/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/app/packages/web/src/content/docs/docs/cli.mdx b/app/packages/web/src/content/docs/docs/cli.mdx new file mode 100644 index 000000000..44a56e1fb --- /dev/null +++ b/app/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/app/packages/web/src/content/docs/docs/config.mdx b/app/packages/web/src/content/docs/docs/config.mdx new file mode 100644 index 000000000..288f194c5 --- /dev/null +++ b/app/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/app/packages/web/src/content/docs/docs/index.mdx b/app/packages/web/src/content/docs/docs/index.mdx new file mode 100644 index 000000000..e6f71be19 --- /dev/null +++ b/app/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/app/packages/web/src/content/docs/docs/lsp-servers.mdx b/app/packages/web/src/content/docs/docs/lsp-servers.mdx new file mode 100644 index 000000000..cd259dea7 --- /dev/null +++ b/app/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/app/packages/web/src/content/docs/docs/mcp-servers.mdx b/app/packages/web/src/content/docs/docs/mcp-servers.mdx new file mode 100644 index 000000000..28c6d2ab2 --- /dev/null +++ b/app/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/app/packages/web/src/content/docs/docs/models.mdx b/app/packages/web/src/content/docs/docs/models.mdx new file mode 100644 index 000000000..c40216695 --- /dev/null +++ b/app/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/app/packages/web/src/content/docs/docs/shortcuts.mdx b/app/packages/web/src/content/docs/docs/shortcuts.mdx new file mode 100644 index 000000000..dd866e0f3 --- /dev/null +++ b/app/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/app/packages/web/src/content/docs/docs/themes.mdx b/app/packages/web/src/content/docs/docs/themes.mdx new file mode 100644 index 000000000..e691a22e7 --- /dev/null +++ b/app/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/app/packages/web/src/content/docs/index.mdx b/app/packages/web/src/content/docs/index.mdx new file mode 100644 index 000000000..176520ec5 --- /dev/null +++ b/app/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/app/packages/web/src/index.css b/app/packages/web/src/index.css deleted file mode 100644 index 85e778f43..000000000 --- a/app/packages/web/src/index.css +++ /dev/null @@ -1,13 +0,0 @@ -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', - 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', - 'Helvetica Neue', sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; -} diff --git a/app/packages/web/src/index.tsx b/app/packages/web/src/index.tsx deleted file mode 100644 index 823bf0fb8..000000000 --- a/app/packages/web/src/index.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* @refresh reload */ -import { render } from "solid-js/web" -import { Router, Route } from "@solidjs/router" - -import "./index.css" -import App from "./App" - -const root = document.getElementById("root") - -if (import.meta.env.DEV && !(root instanceof HTMLElement)) { - throw new Error( - "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?", - ) -} - -render( - () => ( - - - - - ), - root!, -) diff --git a/app/packages/web/src/logo.svg b/app/packages/web/src/logo.svg deleted file mode 100644 index 025aa303c..000000000 --- a/app/packages/web/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/packages/web/src/pages/share/index.astro b/app/packages/web/src/pages/share/index.astro new file mode 100644 index 000000000..f8364b616 --- /dev/null +++ b/app/packages/web/src/pages/share/index.astro @@ -0,0 +1,39 @@ +--- +import config from "virtual:starlight/user-config"; + +import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'; +import Share from "../../components/Share.tsx"; + +// export const prerender = false; + +const { id } = Astro.params; +console.log(Astro.url); +console.log(import.meta.env.VITE_API_URL); + +//export function getStaticPaths() { +// return [ +// { params: { slug: undefined }} +// ]; +//} +--- + + + + + + diff --git a/app/packages/web/src/sst-env.d.ts b/app/packages/web/src/sst-env.d.ts deleted file mode 100644 index 4addc8ef9..000000000 --- a/app/packages/web/src/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/// -interface ImportMetaEnv { - readonly VITE_API_URL: string -} -interface ImportMeta { - readonly env: ImportMetaEnv -} \ No newline at end of file -- cgit v1.2.3