mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-10 03:19:59 -04:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e317e7e481 | |||
| 287855336d | |||
| d55f4f3322 | |||
| b708d0ecec | |||
| afb831c93c | |||
| 14397651b5 | |||
| e5804f64f9 | |||
| ce7b73170f | |||
| 9554abb56e | |||
| d0f5c825bd | |||
| 9f603e39a6 | |||
| da51c9dfac | |||
| 9e04ff013c | |||
| 6bfccace0c |
@@ -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) |
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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,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,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,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.37",
|
||||
"version": "1.0.41",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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,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,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.0.37",
|
||||
"version": "1.0.41",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,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()
|
||||
},
|
||||
|
||||
@@ -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"]) {
|
||||
|
||||
@@ -3,21 +3,77 @@ import { unique } from "remeda"
|
||||
import type { JSONSchema } from "zod/v4/core"
|
||||
|
||||
export namespace ProviderTransform {
|
||||
function normalizeToolCallIds(msgs: ModelMessage[]): ModelMessage[] {
|
||||
return msgs.map((msg) => {
|
||||
if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) {
|
||||
msg.content = msg.content.map((part) => {
|
||||
if ((part.type === "tool-call" || part.type === "tool-result") && "toolCallId" in part) {
|
||||
return {
|
||||
...part,
|
||||
toolCallId: part.toolCallId.replace(/[^a-zA-Z0-9_-]/g, "_"),
|
||||
function normalizeMessages(
|
||||
msgs: ModelMessage[],
|
||||
providerID: string,
|
||||
modelID: string,
|
||||
): ModelMessage[] {
|
||||
if (modelID.includes("claude")) {
|
||||
return msgs.map((msg) => {
|
||||
if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) {
|
||||
msg.content = msg.content.map((part) => {
|
||||
if (
|
||||
(part.type === "tool-call" || part.type === "tool-result") &&
|
||||
"toolCallId" in part
|
||||
) {
|
||||
return {
|
||||
...part,
|
||||
toolCallId: part.toolCallId.replace(/[^a-zA-Z0-9_-]/g, "_"),
|
||||
}
|
||||
}
|
||||
}
|
||||
return part
|
||||
})
|
||||
return part
|
||||
})
|
||||
}
|
||||
return msg
|
||||
})
|
||||
}
|
||||
if (providerID === "mistral" || modelID.toLowerCase().includes("mistral")) {
|
||||
const result: ModelMessage[] = []
|
||||
for (let i = 0; i < msgs.length; i++) {
|
||||
const msg = msgs[i]
|
||||
const prevMsg = msgs[i - 1]
|
||||
const nextMsg = msgs[i + 1]
|
||||
|
||||
if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) {
|
||||
msg.content = msg.content.map((part) => {
|
||||
if (
|
||||
(part.type === "tool-call" || part.type === "tool-result") &&
|
||||
"toolCallId" in part
|
||||
) {
|
||||
// Mistral requires alphanumeric tool call IDs with exactly 9 characters
|
||||
const normalizedId = part.toolCallId
|
||||
.replace(/[^a-zA-Z0-9]/g, "") // Remove non-alphanumeric characters
|
||||
.substring(0, 9) // Take first 9 characters
|
||||
.padEnd(9, "0") // Pad with zeros if less than 9 characters
|
||||
|
||||
return {
|
||||
...part,
|
||||
toolCallId: normalizedId,
|
||||
}
|
||||
}
|
||||
return part
|
||||
})
|
||||
}
|
||||
|
||||
result.push(msg)
|
||||
|
||||
// Fix message sequence: tool messages cannot be followed by user messages
|
||||
if (msg.role === "tool" && nextMsg?.role === "user") {
|
||||
result.push({
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Done.",
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
}
|
||||
return msg
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
return msgs
|
||||
}
|
||||
|
||||
function applyCaching(msgs: ModelMessage[], providerID: string): ModelMessage[] {
|
||||
@@ -64,9 +120,7 @@ export namespace ProviderTransform {
|
||||
}
|
||||
|
||||
export function message(msgs: ModelMessage[], providerID: string, modelID: string) {
|
||||
if (modelID.includes("claude")) {
|
||||
msgs = normalizeToolCallIds(msgs)
|
||||
}
|
||||
msgs = normalizeMessages(msgs, providerID, modelID)
|
||||
if (providerID === "anthropic" || modelID.includes("anthropic") || modelID.includes("claude")) {
|
||||
msgs = applyCaching(msgs, providerID)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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" &&
|
||||
|
||||
@@ -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]) ?? []
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -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,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",
|
||||
@@ -24,4 +24,4 @@
|
||||
"typescript": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
@@ -26,4 +26,4 @@
|
||||
"publishConfig": {
|
||||
"directory": "dist"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.37",
|
||||
"version": "1.0.41",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/components/index.ts",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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 }
|
||||
Reference in New Issue
Block a user