summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-05-27 15:20:43 -0400
committerDax Raad <[email protected]>2025-05-27 15:20:43 -0400
commite98f915fd512e5319079d7b0826ecd44f2d6e463 (patch)
tree257e18551df78111a6600c997f6487546645c33e
parent07f0fea4bf86044439e89673cdab408b231a81a3 (diff)
downloadopencode-e98f915fd512e5319079d7b0826ecd44f2d6e463.tar.gz
opencode-e98f915fd512e5319079d7b0826ecd44f2d6e463.zip
stripped
-rw-r--r--app/package.json8
-rw-r--r--app/packages/function/src/api.ts35
-rw-r--r--app/packages/web/src/components/Share.tsx566
-rw-r--r--js/src/lsp/index.ts2
-rw-r--r--js/src/share/share.ts2
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>&mdash;</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>&mdash;</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>&mdash;</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>&mdash;</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",