diff options
| author | Dax Raad <[email protected]> | 2025-05-27 15:20:43 -0400 |
|---|---|---|
| committer | Dax Raad <[email protected]> | 2025-05-27 15:20:43 -0400 |
| commit | e98f915fd512e5319079d7b0826ecd44f2d6e463 (patch) | |
| tree | 257e18551df78111a6600c997f6487546645c33e | |
| parent | 07f0fea4bf86044439e89673cdab408b231a81a3 (diff) | |
| download | opencode-e98f915fd512e5319079d7b0826ecd44f2d6e463.tar.gz opencode-e98f915fd512e5319079d7b0826ecd44f2d6e463.zip | |
stripped
| -rw-r--r-- | app/package.json | 8 | ||||
| -rw-r--r-- | app/packages/function/src/api.ts | 35 | ||||
| -rw-r--r-- | app/packages/web/src/components/Share.tsx | 566 | ||||
| -rw-r--r-- | js/src/lsp/index.ts | 2 | ||||
| -rw-r--r-- | js/src/share/share.ts | 2 |
5 files changed, 71 insertions, 542 deletions
diff --git a/app/package.json b/app/package.json index 6039a854e..1e4954367 100644 --- a/app/package.json +++ b/app/package.json @@ -1,10 +1,12 @@ { + "$schema": "https://json.schemastore.org/package.json", "name": "opencontrol", "private": true, "type": "module", - "packageManager": "bun", - "description": "OpenCode", - "scripts": {}, + "packageManager": "[email protected]", + "scripts": { + "dev": "sst dev" + }, "workspaces": [ "packages/*" ], diff --git a/app/packages/function/src/api.ts b/app/packages/function/src/api.ts index 35d4902a0..e126cc08a 100644 --- a/app/packages/function/src/api.ts +++ b/app/packages/function/src/api.ts @@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto" import { Resource } from "sst" type Bindings = { - SYNC_SERVER: DurableObjectNamespace + SYNC_SERVER: DurableObjectNamespace<SyncServer> } export class SyncServer extends DurableObject { @@ -17,9 +17,9 @@ export class SyncServer extends DurableObject { setTimeout(async () => { const data = await this.ctx.storage.list() - data.forEach((content, key) => { + data.forEach((content: any, key) => { if (key === "shareID") return - server.send(JSON.stringify({ key, content })) + server.send(JSON.stringify({ key, content: JSON.parse(content) })) }) }, 0) @@ -48,7 +48,7 @@ export class SyncServer extends DurableObject { } async getShareID() { - return this.ctx.storage.get("shareID") + return this.ctx.storage.get<string>("shareID") } async clear() { @@ -59,14 +59,17 @@ export class SyncServer extends DurableObject { export default { async fetch(request: Request, env: Bindings, ctx: ExecutionContext) { const url = new URL(request.url) + const splits = url.pathname.split("/") + const method = splits[1] - if (request.method === "GET" && url.pathname === "/") { + if (request.method === "GET" && method === "") { return new Response("Hello, world!", { headers: { "Content-Type": "text/plain" }, }) } - if (request.method === "POST" && url.pathname.endsWith("/share_create")) { - const body = await request.json() + + if (request.method === "POST" && method === "share_create") { + const body = await request.json<any>() const sessionID = body.sessionID // Get existing shareID @@ -82,8 +85,9 @@ export default { headers: { "Content-Type": "application/json" }, }) } - if (request.method === "POST" && url.pathname.endsWith("/share_delete")) { - const body = await request.json() + + if (request.method === "POST" && method === "share_delete") { + const body = await request.json<any>() const sessionID = body.sessionID const shareID = body.shareID @@ -103,8 +107,9 @@ export default { headers: { "Content-Type": "application/json" }, }) } - if (request.method === "POST" && url.pathname.endsWith("/share_sync")) { - const body = await request.json() + + if (request.method === "POST" && method === "share_sync") { + const body = await request.json<any>() const sessionID = body.sessionID const shareID = body.shareID const key = body.key @@ -132,13 +137,17 @@ export default { await stub.publish(key, content) // store message - await Resource.Bucket.put(`${shareID}/${key}.json`, content) + await Resource.Bucket.put( + `${shareID}/${key}.json`, + JSON.stringify(content), + ) return new Response(JSON.stringify({}), { headers: { "Content-Type": "application/json" }, }) } - if (request.method === "GET" && url.pathname.endsWith("/share_poll")) { + + if (request.method === "GET" && method === "share_poll") { // Expect to receive a WebSocket Upgrade request. // If there is one, accept the request and return a WebSocket Response. const upgradeHeader = request.headers.get("Upgrade") diff --git a/app/packages/web/src/components/Share.tsx b/app/packages/web/src/components/Share.tsx index ae163e81d..c355fa20a 100644 --- a/app/packages/web/src/components/Share.tsx +++ b/app/packages/web/src/components/Share.tsx @@ -1,12 +1,13 @@ -import { createSignal, onCleanup, onMount, Show, For } from "solid-js" +import { createSignal, onCleanup, onMount, Show, For, createMemo } from "solid-js" import styles from "./share.module.css" import { type UIMessage } from "ai" +import { createStore } from "solid-js/store" type Status = "disconnected" | "connecting" | "connected" | "error" | "reconnecting" type Message = { key: string - content: string + content: any } type SessionInfo = { @@ -32,11 +33,27 @@ export default function Share(props: { api: string }) { let params = new URLSearchParams(document.location.search) const sessionId = params.get("id") + const [store, setStore] = createStore<{ + info?: SessionInfo + messages: Record<string, UIMessage<{ + time: { + created: number; + completed?: number; + }; + sessionID: string; + tool: Record<string, { + properties: Record<string, any>; + time: { + start: number; + end: number; + }; + }>; + }>> + }>({ + messages: {}, + }) + const messages = createMemo(() => Object.values(store.messages).toSorted((a, b) => a.id.localeCompare(b.id))) const [connectionStatus, setConnectionStatus] = createSignal<[Status, string?]>(["disconnected", "Disconnected"]) - const [sessionInfo, setSessionInfo] = createSignal<SessionInfo | null>(null) - const [systemMessage, setSystemMessage] = createSignal<Message | null>(null) - const [messages, setMessages] = createSignal<Message[]>([]) - const [expandedSystemMessage, setExpandedSystemMessage] = createSignal(false) onMount(() => { const apiUrl = props.api @@ -87,37 +104,19 @@ export default function Share(props: { api: string }) { console.log("WebSocket message received") try { const data = JSON.parse(event.data) as Message + const [root, type, ...splits] = data.key.split("/") + if (root !== "session") return - // 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) + if (type === "info") { + setStore("info", data.content) 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 + if (type === "message") { + const [, messageID] = splits + setStore("messages", messageID, data.content) } - // 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) } @@ -169,33 +168,25 @@ export default function Share(props: { api: string }) { <div data-section="row"> <ul class={styles.stats}> <li> - <span>Cost</span> - {sessionInfo()?.cost ? - <span>{sessionInfo()?.cost}</span> - : - <span data-placeholder>—</span> - } - </li> - <li> <span>Input Tokens</span> - {sessionInfo()?.tokens?.input ? - <span>{sessionInfo()?.tokens?.input}</span> + {store.info?.tokens?.input ? + <span>{store.info?.tokens?.input}</span> : <span data-placeholder>—</span> } </li> <li> <span>Output Tokens</span> - {sessionInfo()?.tokens?.output ? - <span>{sessionInfo()?.tokens?.output}</span> + {store.info?.tokens?.output ? + <span>{store.info?.tokens?.output}</span> : <span data-placeholder>—</span> } </li> <li> <span>Reasoning Tokens</span> - {sessionInfo()?.tokens?.reasoning ? - <span>{sessionInfo()?.tokens?.reasoning}</span> + {store.info?.tokens?.reasoning ? + <span>{store.info?.tokens?.reasoning}</span> : <span data-placeholder>—</span> } @@ -208,506 +199,31 @@ export default function Share(props: { api: string }) { </div> <div style={{ margin: "2rem 0" }}> - - {/* Display system message as context in the Session Information block */} - <Show when={systemMessage()}> - <div - style={{ - padding: "1rem", - marginBottom: "1rem", - border: "1px solid #dee2e6", - }} - > - <h4 style={{ margin: "0 0 0.75rem 0" }}>Context</h4> - {(() => { - 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 ( - <> - <div - style={{ - padding: "0.75rem", - border: "1px solid #dee2e6", - }} - > - {/* 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 ( - <> - <pre>{modifiedPart.text}</pre> - {hasMoreLines && !expandedSystemMessage() && ( - <div - style={{ - color: "#6c757d", - fontStyle: "italic", - marginTop: "0.5rem", - }} - > - {lines.length - 5} more lines... - </div> - )} - </> - ) - })()} - </div> - {hasMoreLines && ( - <button - onClick={() => - setExpandedSystemMessage(!expandedSystemMessage()) - } - style={{ - marginTop: "0.5rem", - padding: "0.25rem 0.75rem", - border: "1px solid #ced4da", - cursor: "pointer", - fontSize: "0.875rem", - }} - > - {expandedSystemMessage() ? "Show Less" : "Show More"} - </button> - )} - </> - ) - } - } catch (e) { - return <div>Error parsing system message</div> - } - - return null - })()} - </div> - </Show> - <div style={{ border: "1px solid #ccc", padding: "1rem", - maxHeight: "500px", - overflowY: "auto", + "overflow-y": "auto", }} > <Show when={messages().length > 0} fallback={<p>Waiting for messages...</p>} > - <ul style={{ listStyleType: "none", padding: 0 }}> + <ul style={{ "list-style-type": "none", padding: 0 }}> <For each={messages()}> {(msg) => ( <li style={{ padding: "0.75rem", margin: "0.75rem 0", - boxShadow: "0 1px 3px rgba(0,0,0,0.1)", + "box-shadow": "0 1px 3px rgba(0,0,0,0.1)", }} > <div> - <strong>Key:</strong> {msg.key} + <strong>Key:</strong> {msg.id} </div> - - {(() => { - 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 ( - <> - <div style={{ marginTop: "0.5rem" }}> - <strong>Full Content:</strong> - <pre - style={{ - padding: "0.5rem", - overflow: "auto", - maxHeight: "150px", - whiteSpace: "pre-wrap", - wordBreak: "break-word", - fontSize: "0.85rem", - }} - > - {JSON.stringify(parsed, null, 2)} - </pre> - </div> - - {parsed.parts && parsed.parts.length > 0 && ( - <div style={{ marginTop: "0.75rem" }}> - <div - style={{ - display: "flex", - justifyContent: "space-between", - alignItems: "center", - padding: "0.25rem 0.5rem", - marginBottom: "0.5rem", - }} - > - <strong> - Role: {parsed.role || "Unknown"} - </strong> - <span - style={{ - fontSize: "0.8rem", - color: "#6c757d", - }} - > - {createdTime} - </span> - </div> - - <div - style={{ - padding: "0.75rem", - border: "1px solid #dee2e6", - }} - > - <For - each={parsed.parts.filter( - (part) => part.type !== "step-start", - )} - > - {(part) => { - if (part.type === "text") { - //{ - // "type": "text", - // "text": "Hello! How can I help you today?" - //} - return ( - <pre> - [{part.type}] {part.text}{" "} - </pre> - ) - } - 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 ( - <pre> - [{part.type}] {part.text} - </pre> - ) - } - if (part.type === "tool-invocation") { - return ( - <div> - <div - style={{ - display: "flex", - justifyContent: "space-between", - alignItems: "center", - marginBottom: "0.3rem", - }} - > - <span> - <pre - style={{ - margin: 0, - display: "inline", - }} - > - [{part.type}] - </pre>{" "} - Tool:{" "} - <strong> - {part.toolInvocation.toolName} - </strong> - </span> - {parsed.metadata?.tool?.[ - part.toolInvocation.toolCallId - ]?.time?.start && - parsed.metadata?.tool?.[ - part.toolInvocation.toolCallId - ]?.time?.end && ( - <span - style={{ - color: "#6c757d", - fontSize: "0.8rem", - }} - > - {( - (new Date( - parsed.metadata?.tool?.[ - part.toolInvocation.toolCallId - ].time.end, - ) - - new Date( - parsed.metadata?.tool?.[ - part.toolInvocation.toolCallId - ].time.start, - )) / - 1000 - ).toFixed(2)} - s - </span> - )} - </div> - {(() => { - if ( - part.toolInvocation.state === - "partial-call" - ) { - //{ - // "type": "tool-invocation", - // "toolInvocation": { - // "state": "partial-call", - // "toolCallId": "tool_abc123", - // "toolName": "searchWeb", - // "argsTextDelta": "{\"query\":\"latest AI news" - // } - //} - return ( - <> - <pre> - { - part.toolInvocation - .argsTextDelta - } - </pre> - <span>...</span> - </> - ) - } - if ( - part.toolInvocation.state === - "call" - ) { - //{ - // "type": "tool-invocation", - // "toolInvocation": { - // "state": "call", - // "toolCallId": "tool_abc123", - // "toolName": "searchWeb", - // "args": { "query": "latest AI news", "count": 3 } - // } - //} - return ( - <pre> - {JSON.stringify( - part.toolInvocation.args, - null, - 2, - )} - </pre> - ) - } - 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 ( - <> - <pre> - {JSON.stringify( - part.toolInvocation - .args, - null, - 2, - )} - </pre> - <pre> - {JSON.stringify( - part.toolInvocation - .result, - null, - 2, - )} - </pre> - </> - ) - } - 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 ( - <> - <pre> - {JSON.stringify( - part.toolInvocation - .args, - null, - 2, - )} - </pre> - <pre> - { - part.toolInvocation - .errorMessage - } - </pre> - </> - ) - } - })()} - </div> - ) - } - 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 ( - <div> - <div> - <span> - <pre>[{part.type}]</pre> - </span> - <span> - Source:{" "} - {part.source.title || - part.source.id} - </span> - </div> - {part.source.url && ( - <div> - <a - href={part.source.url} - target="_blank" - rel="noopener noreferrer" - style={{ color: "#0c5460" }} - > - {part.source.url} - </a> - </div> - )} - {part.source.sourceType && ( - <div> - Type: {part.source.sourceType} - </div> - )} - </div> - ) - } - 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 ( - <div> - <div> - <span> - <pre>[{part.type}]</pre> - </span> - <span>File: {part.filename}</span> - <span>{part.mediaType}</span> - </div> - - {isImage && part.url ? ( - <div> - <img - src={part.url} - alt={ - part.filename || - "Attached image" - } - /> - </div> - ) : ( - <div> - {part.url ? ( - <a - href={part.url} - target="_blank" - rel="noopener noreferrer" - > - Download: {part.filename} - </a> - ) : ( - <div> - File attachment (no URL - available) - </div> - )} - </div> - )} - </div> - ) - } - return null - }} - </For> - </div> - </div> - )} - </> - ) - } catch (e) { - return ( - <div> - <strong>Content:</strong> - <pre - style={{ - padding: "0.5rem", - overflow: "auto", - maxHeight: "200px", - whiteSpace: "pre-wrap", - wordBreak: "break-word", - }} - > - {msg.content} - </pre> - </div> - ) - } - })()} + <pre>{JSON.stringify(msg, null, 2)}</pre> </li> )} </For> diff --git a/js/src/lsp/index.ts b/js/src/lsp/index.ts index 2294d439f..f8b019e04 100644 --- a/js/src/lsp/index.ts +++ b/js/src/lsp/index.ts @@ -102,11 +102,13 @@ export namespace LSP { ".ctsx", ], }, + /* { id: "golang", command: ["gopls"], extensions: [".go"], }, + */ ]; export namespace Diagnostic { diff --git a/js/src/share/share.ts b/js/src/share/share.ts index db204ab97..f8e25e07d 100644 --- a/js/src/share/share.ts +++ b/js/src/share/share.ts @@ -52,7 +52,7 @@ export namespace Share { await state(); } - const URL = "https://api.dev.opencode.ai"; + const URL = process.env["OPENCODE_API"] ?? "https://api.dev.opencode.ai"; export async function create(sessionID: string) { return fetch(`${URL}/share_create`, { method: "POST", |
