Compare commits

...

17 Commits

Author SHA1 Message Date
opencode e317e7e481 release: v1.0.41 2025-11-07 18:11:39 +00:00
Dax Raad 287855336d allow not specifying a limit on messages endpoint 2025-11-07 13:05:19 -05:00
opencode d55f4f3322 release: v1.0.40 2025-11-07 17:37:31 +00:00
Dax Raad b708d0ecec disable scrollbar temporarily because of text wrap issues 2025-11-07 12:31:28 -05:00
Jinhyeok Lee afb831c93c vscode: Add VS Code Insiders support (#4019) 2025-11-07 10:31:16 -06:00
Adam 14397651b5 ignore: test file 2025-11-07 09:05:09 -06:00
Adam e5804f64f9 fix(desktop): layout quirks 2025-11-07 06:26:26 -06:00
GitHub Action ce7b73170f ignore: update download stats 2025-11-07 2025-11-07 12:04:25 +00:00
Dax Raad 9554abb56e message storage performance improvements 2025-11-07 01:11:47 -05:00
Charles David Mupende d0f5c825bd feat: implement network IP retrieval for remote access in web command (#3945) 2025-11-06 19:37:23 -06:00
opencode-agent[bot] 9f603e39a6 Fixed ACP to respect user's default model config. (#4006)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-06 19:07:27 -06:00
GitHub Action da51c9dfac chore: format code 2025-11-07 01:06:38 +00:00
Nicolai van der Smagt 9e04ff013c fix: resolve Mistral API compatibility issues (#2440)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-06 20:05:59 -05:00
opencode 6bfccace0c release: v1.0.39 2025-11-07 01:04:05 +00:00
Dax Raad b25d4f9dfb fix issue with input randomly breaking 2025-11-06 19:48:10 -05:00
GitHub Action d1962ca5a7 chore: format code 2025-11-07 00:14:59 +00:00
Dax Raad 25f31f3096 codex tweaks 2025-11-06 19:14:01 -05:00
33 changed files with 533 additions and 91 deletions
+1
View File
@@ -132,3 +132,4 @@
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
+11 -11
View File
@@ -39,7 +39,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.0.37",
"version": "1.0.41",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -66,7 +66,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.0.37",
"version": "1.0.41",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -90,7 +90,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.0.37",
"version": "1.0.41",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -111,7 +111,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.0.37",
"version": "1.0.41",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -151,7 +151,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.0.37",
"version": "1.0.41",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
@@ -167,7 +167,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.0.37",
"version": "1.0.41",
"bin": {
"opencode": "./bin/opencode",
},
@@ -245,7 +245,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.0.37",
"version": "1.0.41",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -265,7 +265,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.0.37",
"version": "1.0.41",
"devDependencies": {
"@hey-api/openapi-ts": "0.81.0",
"@tsconfig/node22": "catalog:",
@@ -276,7 +276,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.0.37",
"version": "1.0.41",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -289,7 +289,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.0.37",
"version": "1.0.41",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -319,7 +319,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.0.37",
"version": "1.0.41",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
+1 -1
View File
@@ -7,7 +7,7 @@
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
"build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
"start": "vinxi start",
"version": "1.0.37"
"version": "1.0.41"
},
"dependencies": {
"@ibm/plex": "6.4.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.0.37",
"version": "1.0.41",
"private": true,
"type": "module",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.0.37",
"version": "1.0.41",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.0.37",
"version": "1.0.41",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/desktop",
"version": "1.0.37",
"version": "1.0.41",
"description": "",
"type": "module",
"scripts": {
+13 -12
View File
@@ -358,12 +358,12 @@ export default function Page() {
return (
<div
classList={{
"w-full grid flex-1 _gap-6 min-h-0": true,
"w-full grid flex-1 min-h-0": true,
"grid-cols-2": local.layout.review.state() === "open",
"max-w-2xl mx-auto": local.layout.review.state() !== "open",
}}
>
<div class="relative px-6 py-2 w-full flex flex-col gap-6 flex-1 min-h-0">
<div class="relative px-6 py-2 w-full flex flex-col gap-6 flex-1 min-h-0 max-w-2xl mx-auto">
<div class="h-8 flex shrink-0 self-stretch items-center justify-end">
<Show when={local.layout.review.state() === "closed" && session.diffs().length}>
<Button icon="layout-right" onClick={local.layout.review.open}>
@@ -373,7 +373,7 @@ export default function Page() {
</div>
<div
classList={{
"flex-1 min-h-0": true,
"flex-1 min-h-0 pb-20": true,
"flex items-start justify-start": local.layout.review.state() === "open",
}}
>
@@ -382,7 +382,7 @@ export default function Page() {
role="list"
classList={{
"mr-8 shrink-0 flex flex-col items-start": true,
"absolute right-full w-60 @7xl:gap-2": local.layout.review.state() !== "open",
"absolute right-full w-60 @7xl:gap-2": true, // local.layout.review.state() !== "open",
"": local.layout.review.state() === "open",
}}
>
@@ -498,7 +498,7 @@ export default function Page() {
<Show when={isActive()}>
<div
data-message={message.id}
class="flex flex-col items-start self-stretch gap-8 pb-50"
class="flex flex-col items-start self-stretch gap-8 pb-20"
>
{/* Title */}
<div class="flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger z-10 pb-1">
@@ -645,6 +645,14 @@ export default function Page() {
}}
</For>
</div>
<div class="absolute inset-x-0 px-6 max-w-2xl flex flex-col justify-center items-center z-50 mx-auto bottom-8">
<PromptInput
ref={(el) => {
inputRef = el
}}
/>
</div>
</div>
</div>
<Show when={local.layout.review.state() === "open"}>
@@ -826,13 +834,6 @@ export default function Page() {
</Show>
</DragOverlay>
</DragDropProvider>
<div class="absolute inset-x-0 px-6 max-w-2xl flex flex-col justify-center items-center z-50 mx-auto bottom-8">
<PromptInput
ref={(el) => {
inputRef = el
}}
/>
</div>
<div class="hidden shrink-0 w-56 p-2 h-full overflow-y-auto">
{/* <FileTree path="" onFileClick={ handleTabClick} /> */}
</div>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.0.37",
"version": "1.0.41",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.0.37",
"version": "1.0.41",
"name": "opencode",
"type": "module",
"private": true,
+29 -9
View File
@@ -32,7 +32,7 @@ import { Command } from "@/command"
import { Agent as Agents } from "@/agent/agent"
import { Permission } from "@/permission"
import { SessionCompaction } from "@/session/compaction"
import type { Config } from "@/config/config"
import { Config } from "@/config/config"
import { MCP } from "@/mcp"
import { Todo } from "@/session/todo"
import { z } from "zod"
@@ -41,6 +41,18 @@ import { LoadAPIKeyError } from "ai"
export namespace ACP {
const log = Log.create({ service: "acp-agent" })
export async function init() {
const model = await defaultModel({})
return {
create: (connection: AgentSideConnection, config: ACPConfig) => {
if (!config.defaultModel) {
config.defaultModel = model
}
return new Agent(connection, config)
},
}
}
export class Agent implements ACPAgent {
private sessionManager = new ACPSessionManager()
private connection: AgentSideConnection
@@ -48,13 +60,6 @@ export namespace ACP {
constructor(connection: AgentSideConnection, config: ACPConfig = {}) {
this.connection = connection
if (!config.defaultModel) {
// default to big pickle
config.defaultModel = {
providerID: "opencode",
modelID: "big-pickle",
}
}
this.config = config
this.setupEventSubscriptions()
}
@@ -685,7 +690,22 @@ export namespace ACP {
async function defaultModel(config: ACPConfig) {
const configured = config.defaultModel
if (configured) return configured
return Provider.defaultModel()
const model = await Config.get()
.then((cfg) => {
if (!cfg.model) return undefined
const parsed = Provider.parseModel(cfg.model)
return {
providerID: parsed.providerID,
modelID: parsed.modelID,
}
})
.catch((error) => {
log.error("failed to load user config for default model", { error })
return undefined
})
return model ?? { providerID: "opencode", modelID: "big-pickle" }
}
function parseUri(
+2 -1
View File
@@ -50,9 +50,10 @@ export const AcpCommand = cmd({
})
const stream = ndJsonStream(input, output)
const agent = await ACP.init()
new AgentSideConnection((conn) => {
return new ACP.Agent(conn)
return agent.create(conn, {})
}, stream)
log.info("setup connection")
@@ -3,7 +3,6 @@ import {
BoxRenderable,
TextareaRenderable,
MouseEvent,
KeyEvent,
PasteEvent,
t,
dim,
@@ -270,6 +270,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
},
async sync(sessionID: string) {
const now = Date.now()
if (store.message[sessionID]) return
console.log("syncing", sessionID)
const [session, messages, todo, diff] = await Promise.all([
sdk.client.session.get({ path: { id: sessionID }, throwOnError: true }),
@@ -684,6 +684,7 @@ export function Session() {
ref={(r) => (scroll = r)}
scrollbarOptions={{
paddingLeft: 2,
visible: false,
trackOptions: {
backgroundColor: theme.backgroundElement,
foregroundColor: theme.border,
@@ -2,7 +2,6 @@ import { cmd } from "@/cli/cmd/cmd"
import { tui } from "./app"
import { Rpc } from "@/util/rpc"
import { type rpc } from "./worker"
import { upgrade } from "@/cli/upgrade"
import { Session } from "@/session"
import { bootstrap } from "@/cli/bootstrap"
import path from "path"
@@ -78,8 +77,6 @@ export const TuiThreadCommand = cmd({
}
await bootstrap(cwd, async () => {
upgrade()
const sessionID = await (async () => {
if (args.continue) {
const it = Session.list()
@@ -3,6 +3,7 @@ import { Server } from "@/server/server"
import { Log } from "@/util/log"
import { Instance } from "@/project/instance"
import { Rpc } from "@/util/rpc"
import { upgrade } from "@/cli/upgrade"
await Log.init({
print: process.argv.includes("--print-logs"),
@@ -25,6 +26,8 @@ process.on("uncaughtException", (e) => {
})
})
upgrade()
let server: Bun.Server<undefined>
export const rpc = {
async server(input: { port: number; hostname: string }) {
+53 -6
View File
@@ -2,6 +2,29 @@ import { Server } from "../../server/server"
import { UI } from "../ui"
import { cmd } from "./cmd"
import open from "open"
import { networkInterfaces } from "os"
function getNetworkIPs() {
const nets = networkInterfaces()
const results: string[] = []
for (const name of Object.keys(nets)) {
const net = nets[name]
if (!net) continue
for (const netInfo of net) {
// Skip internal and non-IPv4 addresses
if (netInfo.internal || netInfo.family !== "IPv4") continue
// Skip Docker bridge networks (typically 172.x.x.x)
if (netInfo.address.startsWith("172.")) continue
results.push(netInfo.address)
}
}
return results
}
export const WebCommand = cmd({
command: "web",
@@ -29,12 +52,36 @@ export const WebCommand = cmd({
UI.empty()
UI.println(UI.logo(" "))
UI.empty()
UI.println(
UI.Style.TEXT_INFO_BOLD + " Web interface: ",
UI.Style.TEXT_NORMAL,
server.url.toString(),
)
open(server.url.toString()).catch(() => {})
if (hostname === "0.0.0.0") {
// Show localhost for local access
const localhostUrl = `http://localhost:${server.port}`
UI.println(
UI.Style.TEXT_INFO_BOLD + " Local access: ",
UI.Style.TEXT_NORMAL,
localhostUrl,
)
// Show network IPs for remote access
const networkIPs = getNetworkIPs()
if (networkIPs.length > 0) {
for (const ip of networkIPs) {
UI.println(
UI.Style.TEXT_INFO_BOLD + " Network access: ",
UI.Style.TEXT_NORMAL,
`http://${ip}:${server.port}`,
)
}
}
// Open localhost in browser
open(localhostUrl.toString()).catch(() => {})
} else {
const displayUrl = server.url.toString()
UI.println(UI.Style.TEXT_INFO_BOLD + " Web interface: ", UI.Style.TEXT_NORMAL, displayUrl)
open(displayUrl).catch(() => {})
}
await new Promise(() => {})
await server.stop()
},
+5 -1
View File
@@ -6,6 +6,7 @@ import { Bus } from "../bus"
const SUPPORTED_IDES = [
{ name: "Windsurf" as const, cmd: "windsurf" },
{ name: "Visual Studio Code - Insiders" as const, cmd: "code-insiders" },
{ name: "Visual Studio Code" as const, cmd: "code" },
{ name: "Cursor" as const, cmd: "cursor" },
{ name: "VSCodium" as const, cmd: "codium" },
@@ -43,7 +44,10 @@ export namespace Ide {
}
export function alreadyInstalled() {
return process.env["OPENCODE_CALLER"] === "vscode"
return (
process.env["OPENCODE_CALLER"] === "vscode" ||
process.env["OPENCODE_CALLER"] === "vscode-insiders"
)
}
export async function install(ide: (typeof SUPPORTED_IDES)[number]["name"]) {
+4 -4
View File
@@ -151,12 +151,12 @@ export namespace ProviderTransform {
}
if (modelID.includes("gpt-5") && !modelID.includes("gpt-5-chat")) {
if (!modelID.includes("codex") && !modelID.includes("gpt-5-pro")) {
result["reasoningEffort"] = "medium"
if (modelID.includes("codex")) {
result["store"] = false
}
if (providerID !== "azure") {
result["textVerbosity"] = modelID.includes("codex") ? "medium" : "low"
if (!modelID.includes("codex") && !modelID.includes("gpt-5-pro")) {
result["reasoningEffort"] = "medium"
}
if (providerID === "opencode") {
+1 -3
View File
@@ -111,9 +111,7 @@ export namespace SessionCompaction {
draft.time.compacting = undefined
})
})
const toSummarize = await Session.messages({ sessionID: input.sessionID }).then(
MessageV2.filterCompacted,
)
const toSummarize = await MessageV2.filterCompacted(Session.messageStream(input.sessionID))
const model = await Provider.getModel(input.providerID, input.modelID)
const system = [
...SystemPrompt.summarize(model.providerID),
+15 -9
View File
@@ -273,6 +273,17 @@ export namespace Session {
return diffs ?? []
})
export const messageStream = fn(Identifier.schema("session"), async function* (sessionID) {
const list = await Array.fromAsync(await Storage.list(["message", sessionID]))
for (let i = list.length - 1; i >= 0; i--) {
const read = await Storage.read<MessageV2.Info>(list[i])
yield {
info: read,
parts: await getParts(read.id),
}
}
})
export const messages = fn(
z.object({
sessionID: Identifier.schema("session"),
@@ -280,16 +291,11 @@ export namespace Session {
}),
async (input) => {
const result = [] as MessageV2.WithParts[]
const list = (await Array.fromAsync(await Storage.list(["message", input.sessionID])))
.toSorted((a, b) => a.at(-1)!.localeCompare(b.at(-1)!))
.slice(-1 * (input.limit ?? 1_000_000))
for (const p of list) {
const read = await Storage.read<MessageV2.Info>(p)
result.push({
info: read,
parts: await getParts(read.id),
})
for await (const msg of messageStream(input.sessionID)) {
if (input.limit && result.length >= input.limit) break
result.push(msg)
}
result.reverse()
return result
},
)
+8 -4
View File
@@ -655,10 +655,14 @@ export namespace MessageV2 {
return convertToModelMessages(result)
}
export function filterCompacted(msgs: { info: MessageV2.Info; parts: MessageV2.Part[] }[]) {
const i = msgs.findLastIndex((m) => m.info.role === "assistant" && !!m.info.summary)
if (i === -1) return msgs.slice()
return msgs.slice(i)
export async function filterCompacted(stream: AsyncIterable<MessageV2.WithParts>) {
const result = [] as MessageV2.WithParts[]
for await (const msg of stream) {
result.push(msg)
if (msg.info.role === "assistant" && msg.info.summary === true) break
}
result.reverse()
return result
}
export function fromError(e: unknown, ctx: { providerID: string }) {
+1 -3
View File
@@ -434,9 +434,7 @@ export namespace SessionPrompt {
providerID: string
signal: AbortSignal
}) {
let msgs = await Session.messages({ sessionID: input.sessionID }).then(
MessageV2.filterCompacted,
)
let msgs = await MessageV2.filterCompacted(Session.messageStream(input.sessionID))
const lastAssistant = msgs.findLast((msg) => msg.info.role === "assistant")
if (
lastAssistant?.info.role === "assistant" &&
+1 -11
View File
@@ -151,17 +151,7 @@ export namespace SessionSummary {
messageID: Identifier.schema("message").optional(),
}),
async (input) => {
let all = await Session.messages({ sessionID: input.sessionID })
if (input.messageID)
all = all.filter(
(x) =>
x.info.id === input.messageID ||
(x.info.role === "assistant" && x.info.parentID === input.messageID),
)
return computeDiff({
messages: all,
})
return Storage.read<Snapshot.FileDiff[]>(["session_diff", input.sessionID]) ?? []
},
)
+86
View File
@@ -0,0 +1,86 @@
import { describe, expect, test, afterEach } from "bun:test"
import { Ide } from "../../src/ide"
describe("ide", () => {
const original = structuredClone(process.env)
afterEach(() => {
Object.keys(process.env).forEach((key) => {
delete process.env[key]
})
Object.assign(process.env, original)
})
test("should detect Visual Studio Code", () => {
process.env["TERM_PROGRAM"] = "vscode"
process.env["GIT_ASKPASS"] =
"/path/to/Visual Studio Code.app/Contents/Resources/app/extensions/git/dist/askpass.sh"
expect(Ide.ide()).toBe("Visual Studio Code")
})
test("should detect Visual Studio Code Insiders", () => {
process.env["TERM_PROGRAM"] = "vscode"
process.env["GIT_ASKPASS"] =
"/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/extensions/git/dist/askpass.sh"
expect(Ide.ide()).toBe("Visual Studio Code - Insiders")
})
test("should detect Cursor", () => {
process.env["TERM_PROGRAM"] = "vscode"
process.env["GIT_ASKPASS"] =
"/path/to/Cursor.app/Contents/Resources/app/extensions/git/dist/askpass.sh"
expect(Ide.ide()).toBe("Cursor")
})
test("should detect VSCodium", () => {
process.env["TERM_PROGRAM"] = "vscode"
process.env["GIT_ASKPASS"] =
"/path/to/VSCodium.app/Contents/Resources/app/extensions/git/dist/askpass.sh"
expect(Ide.ide()).toBe("VSCodium")
})
test("should detect Windsurf", () => {
process.env["TERM_PROGRAM"] = "vscode"
process.env["GIT_ASKPASS"] =
"/path/to/Windsurf.app/Contents/Resources/app/extensions/git/dist/askpass.sh"
expect(Ide.ide()).toBe("Windsurf")
})
test("should return unknown when TERM_PROGRAM is not vscode", () => {
process.env["TERM_PROGRAM"] = "iTerm2"
process.env["GIT_ASKPASS"] =
"/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/extensions/git/dist/askpass.sh"
expect(Ide.ide()).toBe("unknown")
})
test("should return unknown when GIT_ASKPASS does not contain IDE name", () => {
process.env["TERM_PROGRAM"] = "vscode"
process.env["GIT_ASKPASS"] = "/path/to/unknown/askpass.sh"
expect(Ide.ide()).toBe("unknown")
})
test("should recognize vscode-insiders OPENCODE_CALLER", () => {
process.env["OPENCODE_CALLER"] = "vscode-insiders"
expect(Ide.alreadyInstalled()).toBe(true)
})
test("should recognize vscode OPENCODE_CALLER", () => {
process.env["OPENCODE_CALLER"] = "vscode"
expect(Ide.alreadyInstalled()).toBe(true)
})
test("should return false for unknown OPENCODE_CALLER", () => {
process.env["OPENCODE_CALLER"] = "unknown"
expect(Ide.alreadyInstalled()).toBe(false)
})
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.0.37",
"version": "1.0.41",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.0.37",
"version": "1.0.41",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.0.37",
"version": "1.0.41",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.0.37",
"version": "1.0.41",
"type": "module",
"exports": {
".": "./src/components/index.ts",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/web",
"type": "module",
"version": "1.0.37",
"version": "1.0.41",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.0.37",
"version": "1.0.41",
"publisher": "sst-dev",
"repository": {
"type": "git",
+285
View File
@@ -0,0 +1,285 @@
import React, { useState, useEffect, useCallback, useMemo } from "react"
import { ComponentType, FC, ReactNode, JSX } from "react"
// Interface definitions
interface User {
id: number
name: string
email?: string
readonly createdAt: Date
active: boolean
}
type Theme = "light" | "dark" | "auto"
type Status = "pending" | "loading" | "success" | "error"
// Generic function with constraints
function createRepository<T extends { id: number }>(items: T[]): Repository<T> {
return new Repository(items)
}
// Class definition
class Repository<T> {
private items: T[]
protected cache: Map<string, T>
constructor(items: T[] = []) {
this.items = items
this.cache = new Map()
}
public find(id: number): T | undefined {
return this.items.find((item) => item.id === id)
}
public async findAll(): Promise<T[]> {
return this.items
}
get count(): number {
return this.items.length
}
}
// Enum definition
enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
}
// Constants
const API_URL = "https://api.example.com"
const MAX_RETRIES = 3
const DEFAULT_TIMEOUT = 5000
// Regular expressions
const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
const PHONE_REGEX = /^\+?1?-?\.?\s?\(?(\d{3})\)?[-.\s]?(\d{3})[-.\s]?(\d{4})$/
// Template literals
const greeting = `Hello, ${user.name}!`
const sql = `
SELECT * FROM users
WHERE active = true
AND created_at > '${new Date().toISOString()}'
`
// Arrow functions
const debounce = <T extends (...args: any[]) => any>(
func: T,
wait: number,
): ((...args: Parameters<T>) => void) => {
let timeout: NodeJS.Timeout
return (...args: Parameters<T>) => {
clearTimeout(timeout)
timeout = setTimeout(() => func(...args), wait)
}
}
// Async function
async function fetchUserData(userId: number): Promise<User> {
try {
const response = await fetch(`${API_URL}/users/${userId}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return await response.json()
} catch (error) {
console.error("Failed to fetch user data:", error)
throw error
}
}
// React component with various patterns
const ThemeProvider: FC<{ children: ReactNode; theme?: Theme }> = ({
children,
theme = "auto",
}) => {
const [currentTheme, setCurrentTheme] = useState<Theme>(theme)
const [status, setStatus] = useState<Status>("pending")
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
const handleChange = (e: MediaQueryListEvent) => {
setCurrentTheme(e.matches ? "dark" : "light")
}
mediaQuery.addEventListener("change", handleChange)
return () => mediaQuery.removeEventListener("change", handleChange)
}, [])
const contextValue = useMemo(
() => ({
theme: currentTheme,
setTheme: setCurrentTheme,
toggleTheme: () => setCurrentTheme((prev) => (prev === "light" ? "dark" : "light")),
}),
[currentTheme],
)
return (
<ThemeContext.Provider value={contextValue}>
<div className={`theme-${currentTheme}`} data-theme={currentTheme}>
{children}
</div>
</ThemeContext.Provider>
)
}
// Higher-order component
function withLogging<P extends object>(Component: ComponentType<P>): ComponentType<P> {
return function LoggedComponent(props: P) {
console.log("Rendering component:", Component.name)
return <Component {...props} />
}
}
// Custom hook
function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.warn(`Error reading localStorage key "${key}":`, error)
return initialValue
}
})
const setValue = useCallback(
(value: T) => {
try {
setStoredValue(value)
window.localStorage.setItem(key, JSON.stringify(value))
} catch (error) {
console.warn(`Error setting localStorage key "${key}":`, error)
}
},
[key],
)
return [storedValue, setValue]
}
// JSX component with various elements
const UserProfile: FC<{ user: User; onUpdate?: (user: User) => void }> = ({ user, onUpdate }) => {
const [isEditing, setIsEditing] = useState(false)
const [formData, setFormData] = useState(user)
const [errors, setErrors] = useState<Record<string, string>>({})
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
// Validation
if (!formData.name.trim()) {
setErrors({ name: "Name is required" })
return
}
if (formData.email && !EMAIL_REGEX.test(formData.email)) {
setErrors({ email: "Invalid email format" })
return
}
try {
setStatus("loading")
await onUpdate?.(formData)
setStatus("success")
setIsEditing(false)
} catch (error) {
setStatus("error")
setErrors({ submit: "Failed to update profile" })
}
}
return (
<div className="user-profile">
<div className="user-header">
<h2>{user.name}</h2>
<span className={`status status-${user.active ? "active" : "inactive"}`}>
{user.active ? "Active" : "Inactive"}
</span>
</div>
{isEditing ? (
<form onSubmit={handleSubmit} className="user-form">
<div className="form-group">
<label htmlFor="name">Name:</label>
<input
id="name"
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className={errors.name ? "error" : ""}
placeholder="Enter your name"
required
/>
{errors.name && <span className="error-message">{errors.name}</span>}
</div>
<div className="form-group">
<label htmlFor="email">Email:</label>
<input
id="email"
type="email"
value={formData.email || ""}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className={errors.email ? "error" : ""}
placeholder="user@example.com"
/>
{errors.email && <span className="error-message">{errors.email}</span>}
</div>
<div className="form-actions">
<button type="submit" disabled={status === "loading"}>
{status === "loading" ? "Saving..." : "Save"}
</button>
<button type="button" onClick={() => setIsEditing(false)} className="secondary">
Cancel
</button>
</div>
{errors.submit && <div className="alert alert-error">{errors.submit}</div>}
</form>
) : (
<div className="user-details">
<p>
<strong>ID:</strong> {user.id}
</p>
<p>
<strong>Email:</strong> {user.email || "Not provided"}
</p>
<p>
<strong>Member since:</strong> {user.createdAt.toLocaleDateString()}
</p>
<button onClick={() => setIsEditing(true)} className="edit-button">
Edit Profile
</button>
</div>
)}
</div>
)
}
// Export statements
export {
User,
Theme,
Status,
LogLevel,
Repository,
ThemeProvider,
UserProfile,
fetchUserData,
useLocalStorage,
withLogging,
debounce,
}
export default ThemeProvider
// Type exports
export type { User as UserType, ComponentType as ReactComponentType }