Compare commits

..

32 Commits

Author SHA1 Message Date
Aiden Cline
2a4c035486 ignore, see hwat happens 2025-12-31 15:23:46 -06:00
Aiden Cline
40a303164e test: add tests for ripgrep 2025-12-31 15:16:16 -06:00
Aiden Cline
e7422ee782 chore: rm unused import 2025-12-31 12:11:42 -06:00
Aiden Cline
dc3e731afe ci: tweak changelog ensure all cmd/ things fall under tui section 2025-12-31 12:05:35 -06:00
GitHub Action
de50e7f9b9 chore: generate 2025-12-31 17:35:47 +00:00
Marcel de Vries
f3db966317 Add support for LSP workspace/didChangeWatchedFiles (#6524)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-31 11:35:09 -06:00
opencode
decc616c80 release: v1.0.220 2025-12-31 17:30:57 +00:00
Aiden Cline
e0450e74fb fix: plugin & mode globs 2025-12-31 11:28:06 -06:00
GitHub Action
094af4dc7d chore: generate 2025-12-31 17:19:02 +00:00
Michael Ramos
2dc14718aa docs: Add plannotator plugin (#6510) 2025-12-31 11:18:28 -06:00
Eric Guo
b97d20f252 feat(cli): New debug agent <name> subcommand (#6529) 2025-12-31 11:16:03 -06:00
Anzul Aqeel
fcfcdd95e9 fix: Clarify agent-name placeholder in tips (#6520) 2025-12-31 11:15:07 -06:00
opencode-agent[bot]
c42bd492ea docs: new configurable CORS option (#6522)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2025-12-31 11:11:20 -06:00
Frank
840fe030ab zen: fix fee instruction 2025-12-31 11:54:01 -05:00
Adam
a7c4f83ca2 fix(desktop): remove status bar, new elements in header 2025-12-31 10:22:17 -06:00
Adam
ed70a07201 fix(desktop): cleanup user message style 2025-12-31 10:22:16 -06:00
Adam
18a5eb205f fix(desktop): better new session button 2025-12-31 10:22:16 -06:00
Paolo Ricciuti
5249f04ea0 fix: display MCP tag for prompts in autocomplete but not in prompt (#6531) 2025-12-31 09:34:36 -06:00
Adam
6e3ead198e fix(ui): radio group styles 2025-12-31 09:26:14 -06:00
GitHub Action
4309d439f5 chore: generate 2025-12-31 15:24:05 +00:00
Adam
2ec6a21cc0 feat(desktop): unified diff toggle 2025-12-31 09:23:24 -06:00
Adam
ebf5ad25c5 fix(desktop): not rendering large sessions 2025-12-31 08:29:36 -06:00
opencode
8aa34ab9f3 release: v1.0.219 2025-12-31 14:01:59 +00:00
Adam
50ef866a02 fix(core): mdns fails if service already registered 2025-12-31 07:33:49 -06:00
Adam
3650fefe2d fix(desktop): don't expand tools by default 2025-12-31 07:10:47 -06:00
GitHub Action
22091c29f1 ignore: update download stats 2025-12-31 2025-12-31 12:04:56 +00:00
Adam
e7e89dc5a6 chore: cleanup 2025-12-31 04:47:24 -06:00
Adam
34e9392bb4 chore: daytona skip preview warning 2025-12-31 04:22:53 -06:00
GitHub Action
05c3bc27ff chore: generate 2025-12-31 09:51:12 +00:00
Adam
b1a6333d17 feat(core): configurable cors hosts 2025-12-31 03:50:29 -06:00
Aiden Cline
5c9d619620 docs: add variants docs (#6516)
Co-authored-by: David Hill <iamdavidhill@gmail.com>
2025-12-31 01:17:50 -06:00
GitHub Action
dfb9caa2a9 chore: generate 2025-12-31 06:51:59 +00:00
56 changed files with 901 additions and 425 deletions

View File

@@ -186,3 +186,4 @@
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) |
| 2025-12-30 | 1,445,450 (+29,890) | 1,272,689 (+15,588) | 2,718,139 (+45,478) |
| 2025-12-31 | 1,479,598 (+34,148) | 1,293,235 (+20,546) | 2,772,833 (+54,694) |

View File

@@ -22,7 +22,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.0.218",
"version": "1.0.220",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -70,7 +70,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.0.218",
"version": "1.0.220",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -98,7 +98,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.0.218",
"version": "1.0.220",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -125,7 +125,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.0.218",
"version": "1.0.220",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -149,7 +149,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.0.218",
"version": "1.0.220",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -173,7 +173,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.0.218",
"version": "1.0.220",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@solid-primitives/storage": "catalog:",
@@ -201,7 +201,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.0.218",
"version": "1.0.220",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -230,7 +230,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.0.218",
"version": "1.0.220",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -246,7 +246,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.0.218",
"version": "1.0.220",
"bin": {
"opencode": "./bin/opencode",
},
@@ -348,7 +348,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.0.218",
"version": "1.0.220",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -368,7 +368,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.0.218",
"version": "1.0.220",
"devDependencies": {
"@hey-api/openapi-ts": "0.88.1",
"@tsconfig/node22": "catalog:",
@@ -379,7 +379,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.0.218",
"version": "1.0.220",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -392,7 +392,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.0.218",
"version": "1.0.220",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -430,7 +430,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.0.218",
"version": "1.0.220",
"dependencies": {
"zod": "catalog:",
},
@@ -441,7 +441,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.0.218",
"version": "1.0.220",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.0.218",
"version": "1.0.220",
"description": "",
"type": "module",
"exports": {

View File

@@ -1,215 +0,0 @@
import { useGlobalSync } from "@/context/global-sync"
import { useGlobalSDK } from "@/context/global-sdk"
import { useLayout } from "@/context/layout"
import { Session } from "@opencode-ai/sdk/v2/client"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { Mark } from "@opencode-ai/ui/logo"
import { Popover } from "@opencode-ai/ui/popover"
import { Select } from "@opencode-ai/ui/select"
import { TextField } from "@opencode-ai/ui/text-field"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { base64Decode } from "@opencode-ai/util/encode"
import { useCommand } from "@/context/command"
import { getFilename } from "@opencode-ai/util/path"
import { A, useParams } from "@solidjs/router"
import { createMemo, createResource, Show } from "solid-js"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { iife } from "@opencode-ai/util/iife"
export function Header(props: {
navigateToProject: (directory: string) => void
navigateToSession: (session: Session | undefined) => void
onMobileMenuToggle?: () => void
}) {
const globalSync = useGlobalSync()
const globalSDK = useGlobalSDK()
const layout = useLayout()
const params = useParams()
const command = useCommand()
return (
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
<button
type="button"
class="xl:hidden w-12 shrink-0 flex items-center justify-center border-r border-border-weak-base hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors"
onClick={props.onMobileMenuToggle}
>
<Icon name="menu" size="small" />
</button>
<A
href="/"
classList={{
"hidden xl:flex": true,
"w-12 shrink-0 px-4 py-3.5": true,
"items-center justify-start self-stretch": true,
"border-r border-border-weak-base": true,
}}
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
data-tauri-drag-region
>
<Mark class="shrink-0" />
</A>
<div class="pl-4 px-6 flex items-center justify-between gap-4 w-full">
<Show when={layout.projects.list().length > 0 && params.dir}>
{(directory) => {
const currentDirectory = createMemo(() => base64Decode(directory()))
const store = createMemo(() => globalSync.child(currentDirectory())[0])
const sessions = createMemo(() => (store().session ?? []).filter((s) => !s.parentID))
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
const shareEnabled = createMemo(() => store().config.share !== "disabled")
return (
<>
<div class="flex items-center gap-3 min-w-0">
<div class="flex items-center gap-2 min-w-0">
<div class="hidden xl:flex items-center gap-2">
<Select
options={layout.projects.list().map((project) => project.worktree)}
current={currentDirectory()}
label={(x) => getFilename(x)}
onSelect={(x) => (x ? props.navigateToProject(x) : undefined)}
class="text-14-regular text-text-base"
variant="ghost"
>
{/* @ts-ignore */}
{(i) => (
<div class="flex items-center gap-2">
<Icon name="folder" size="small" />
<div class="text-text-strong">{getFilename(i)}</div>
</div>
)}
</Select>
<div class="text-text-weaker">/</div>
</div>
<Select
options={sessions()}
current={currentSession()}
placeholder="New session"
label={(x) => x.title}
value={(x) => x.id}
onSelect={props.navigateToSession}
class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
variant="ghost"
/>
</div>
<Show when={currentSession()}>
<Tooltip
class="hidden xl:block"
value={
<div class="flex items-center gap-2">
<span>New session</span>
<span class="text-icon-base text-12-medium">{command.keybind("session.new")}</span>
</div>
}
>
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
New session
</Button>
</Tooltip>
</Show>
</div>
<div class="flex items-center gap-4">
<Show when={currentSession()?.summary?.files}>
<Tooltip
class="hidden md:block shrink-0"
value={
<div class="flex items-center gap-2">
<span>Toggle review</span>
<span class="text-icon-base text-12-medium">{command.keybind("review.toggle")}</span>
</div>
}
>
<Button variant="ghost" class="group/review-toggle size-6 p-0" onClick={layout.review.toggle}>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
name={layout.review.opened() ? "layout-right" : "layout-left"}
size="small"
class="group-hover/review-toggle:hidden"
/>
<Icon
name={layout.review.opened() ? "layout-right-partial" : "layout-left-partial"}
size="small"
class="hidden group-hover/review-toggle:inline-block"
/>
<Icon
name={layout.review.opened() ? "layout-right-full" : "layout-left-full"}
size="small"
class="hidden group-active/review-toggle:inline-block"
/>
</div>
</Button>
</Tooltip>
</Show>
<Tooltip
class="hidden md:block shrink-0"
value={
<div class="flex items-center gap-2">
<span>Toggle terminal</span>
<span class="text-icon-base text-12-medium">{command.keybind("terminal.toggle")}</span>
</div>
}
>
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
class="group-hover/terminal-toggle:hidden"
/>
<Icon
size="small"
name="layout-bottom-partial"
class="hidden group-hover/terminal-toggle:inline-block"
/>
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
class="hidden group-active/terminal-toggle:inline-block"
/>
</div>
</Button>
</Tooltip>
<Show when={shareEnabled() && currentSession()}>
<Popover
title="Share session"
trigger={
<Tooltip class="shrink-0" value="Share session">
<IconButton icon="share" variant="ghost" class="" />
</Tooltip>
}
>
{iife(() => {
const [url] = createResource(
() => currentSession(),
async (session) => {
if (!session) return
let shareURL = session.share?.url
if (!shareURL) {
shareURL = await globalSDK.client.session
.share({ sessionID: session.id, directory: currentDirectory() })
.then((r) => r.data?.share?.url)
.catch((e) => {
console.error("Failed to share session", e)
return undefined
})
}
return shareURL
},
)
return (
<Show when={url()}>
{(url) => <TextField value={url()} readOnly copyable class="w-72" />}
</Show>
)
})}
</Popover>
</Show>
</div>
</>
)
}}
</Show>
</div>
</header>
)
}

View File

@@ -1,53 +0,0 @@
import { createMemo, Show, type ParentProps } from "solid-js"
import { useSync } from "@/context/sync"
import { useGlobalSync } from "@/context/global-sync"
import { useServer } from "@/context/server"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Button } from "@opencode-ai/ui/button"
import { DialogSelectServer } from "@/components/dialog-select-server"
export function StatusBar(props: ParentProps) {
const dialog = useDialog()
const server = useServer()
const sync = useSync()
const globalSync = useGlobalSync()
const directoryDisplay = createMemo(() => {
const directory = sync.data.path.directory || ""
const home = globalSync.data.path.home || ""
const short = home && directory.startsWith(home) ? directory.replace(home, "~") : directory
const branch = sync.data.vcs?.branch
return branch ? `${short}:${branch}` : short
})
return (
<div class="h-8 w-full shrink-0 flex items-center justify-between px-2 border-t border-border-weak-base bg-background-base">
<div class="flex items-center gap-3">
<div class="flex items-center gap-1">
<Button
size="small"
variant="ghost"
onClick={() => {
dialog.show(() => <DialogSelectServer />)
}}
>
<div
classList={{
"size-1.5 rounded-full": true,
"bg-icon-success-base": server.healthy() === true,
"bg-icon-critical-base": server.healthy() === false,
"bg-border-weak-base": server.healthy() === undefined,
}}
/>
<span class="text-12-regular text-text-weak">{server.name}</span>
</Button>
</div>
<Show when={directoryDisplay()}>
<span class="text-12-regular text-text-weak">{directoryDisplay()}</span>
</Show>
</div>
<div class="flex items-center">{props.children}</div>
</div>
)
}

View File

@@ -30,6 +30,8 @@ type SessionTabs = {
export type LocalProject = Partial<Project> & { worktree: string; expanded: boolean }
export type ReviewDiffStyle = "unified" | "split"
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
name: "Layout",
init: () => {
@@ -49,6 +51,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
review: {
opened: true,
diffStyle: "split" as ReviewDiffStyle,
},
session: {
width: 600,
@@ -156,6 +159,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
review: {
opened: createMemo(() => store.review?.opened ?? true),
diffStyle: createMemo(() => store.review?.diffStyle ?? "split"),
setDiffStyle(diffStyle: ReviewDiffStyle) {
if (!store.review) {
setStore("review", { opened: true, diffStyle })
return
}
setStore("review", "diffStyle", diffStyle)
},
open() {
setStore("review", "opened", true)
},

View File

@@ -63,7 +63,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
async sync(sessionID: string, _isRetry = false) {
const [session, messages, todo, diff] = await Promise.all([
retry(() => sdk.client.session.get({ sessionID })),
retry(() => sdk.client.session.messages({ sessionID, limit: 100 })),
retry(() => sdk.client.session.messages({ sessionID, limit: 1000 })),
retry(() => sdk.client.session.todo({ sessionID })),
retry(() => sdk.client.session.diff({ sessionID })),
])

View File

@@ -26,6 +26,7 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Collapsible } from "@opencode-ai/ui/collapsible"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { Spinner } from "@opencode-ai/ui/spinner"
import { Mark } from "@opencode-ai/ui/logo"
import { getFilename } from "@opencode-ai/util/path"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Session } from "@opencode-ai/sdk/v2/client"
@@ -45,7 +46,7 @@ import { showToast, Toast, toaster } from "@opencode-ai/ui/toast"
import { useGlobalSDK } from "@/context/global-sdk"
import { useNotification } from "@/context/notification"
import { Binary } from "@opencode-ai/util/binary"
import { Header } from "@/components/header"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
import { DialogSelectProvider } from "@/components/dialog-select-provider"
@@ -873,6 +874,11 @@ export default function Layout(props: ParentProps) {
return (
<>
<div class="flex flex-col items-start self-stretch gap-4 p-2 min-h-0 overflow-hidden">
<Show when={!sidebarProps.mobile}>
<A href="/" class="shrink-0 h-8 flex items-center justify-start px-2" data-tauri-drag-region>
<Mark class="shrink-0" />
</A>
</Show>
<Show when={!sidebarProps.mobile}>
<Tooltip
class="shrink-0"
@@ -1018,11 +1024,6 @@ export default function Layout(props: ParentProps) {
return (
<div class="relative flex-1 min-h-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
<Header
navigateToProject={navigateToProject}
navigateToSession={navigateToSession}
onMobileMenuToggle={mobileSidebar.toggle}
/>
<div class="flex-1 min-h-0 flex">
<div
classList={{

View File

@@ -51,17 +51,26 @@ import { DialogSelectFile } from "@/components/dialog-select-file"
import { DialogSelectModel } from "@/components/dialog-select-model"
import { DialogSelectMcp } from "@/components/dialog-select-mcp"
import { useCommand } from "@/context/command"
import { useNavigate, useParams } from "@solidjs/router"
import { A, useNavigate, useParams } from "@solidjs/router"
import { UserMessage } from "@opencode-ai/sdk/v2"
import { useSDK } from "@/context/sdk"
import { usePrompt } from "@/context/prompt"
import { extractPromptFromParts } from "@/utils/prompt"
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
import { StatusBar } from "@/components/status-bar"
import { SessionMcpIndicator } from "@/components/session-mcp-indicator"
import { SessionLspIndicator } from "@/components/session-lsp-indicator"
import { usePermission } from "@/context/permission"
import { showToast } from "@opencode-ai/ui/toast"
import { useServer } from "@/context/server"
import { Button } from "@opencode-ai/ui/button"
import { DialogSelectServer } from "@/components/dialog-select-server"
import { SessionLspIndicator } from "@/components/session-lsp-indicator"
import { SessionMcpIndicator } from "@/components/session-mcp-indicator"
import { useGlobalSDK } from "@/context/global-sdk"
import { Popover } from "@opencode-ai/ui/popover"
import { Select } from "@opencode-ai/ui/select"
import { TextField } from "@opencode-ai/ui/text-field"
import { base64Encode } from "@opencode-ai/util/encode"
import { iife } from "@opencode-ai/util/iife"
import { Session } from "@opencode-ai/sdk/v2/client"
function same<T>(a: readonly T[], b: readonly T[]) {
if (a === b) return true
@@ -69,6 +78,212 @@ function same<T>(a: readonly T[], b: readonly T[]) {
return a.every((x, i) => x === b[i])
}
function Header(props: { onMobileMenuToggle?: () => void }) {
const globalSDK = useGlobalSDK()
const layout = useLayout()
const params = useParams()
const navigate = useNavigate()
const command = useCommand()
const server = useServer()
const dialog = useDialog()
const sync = useSync()
const sessions = createMemo(() => (sync.data.session ?? []).filter((s) => !s.parentID))
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
const branch = createMemo(() => sync.data.vcs?.branch)
function navigateToProject(directory: string) {
navigate(`/${base64Encode(directory)}`)
}
function navigateToSession(session: Session | undefined) {
if (!session) return
navigate(`/${params.dir}/session/${session.id}`)
}
return (
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
<button
type="button"
class="xl:hidden w-12 shrink-0 flex items-center justify-center border-r border-border-weak-base hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors"
onClick={props.onMobileMenuToggle}
>
<Icon name="menu" size="small" />
</button>
<div class="px-4 flex items-center justify-between gap-4 w-full">
<div class="flex items-center gap-3 min-w-0">
<div class="flex items-center gap-2 min-w-0">
<div class="hidden xl:flex items-center gap-2">
<Select
options={layout.projects.list().map((project) => project.worktree)}
current={sync.directory}
label={(x) => {
const name = getFilename(x)
const b = x === sync.directory ? branch() : undefined
return b ? `${name}:${b}` : name
}}
onSelect={(x) => (x ? navigateToProject(x) : undefined)}
class="text-14-regular text-text-base"
variant="ghost"
>
{/* @ts-ignore */}
{(i) => (
<div class="flex items-center gap-2">
<Icon name="folder" size="small" />
<div class="text-text-strong">{getFilename(i)}</div>
</div>
)}
</Select>
<div class="text-text-weaker">/</div>
</div>
<Select
options={sessions()}
current={currentSession()}
placeholder="New session"
label={(x) => x.title}
value={(x) => x.id}
onSelect={navigateToSession}
class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
variant="ghost"
/>
</div>
<Show when={currentSession()}>
<Tooltip
class="hidden xl:block"
value={
<div class="flex items-center gap-2">
<span>New session</span>
<span class="text-icon-base text-12-medium">{command.keybind("session.new")}</span>
</div>
}
>
<IconButton as={A} href={`/${params.dir}/session`} icon="edit-small-2" variant="ghost" />
</Tooltip>
</Show>
</div>
<div class="flex items-center gap-3">
<div class="hidden md:flex items-center gap-1">
<Button
size="small"
variant="ghost"
onClick={() => {
dialog.show(() => <DialogSelectServer />)
}}
>
<div
classList={{
"size-1.5 rounded-full": true,
"bg-icon-success-base": server.healthy() === true,
"bg-icon-critical-base": server.healthy() === false,
"bg-border-weak-base": server.healthy() === undefined,
}}
/>
<Icon name="server" size="small" class="text-icon-weak" />
<span class="text-12-regular text-text-weak truncate max-w-[200px]">{server.name}</span>
</Button>
<SessionLspIndicator />
<SessionMcpIndicator />
</div>
<div class="flex items-center gap-1">
<Show when={currentSession()?.summary?.files}>
<Tooltip
class="hidden md:block shrink-0"
value={
<div class="flex items-center gap-2">
<span>Toggle review</span>
<span class="text-icon-base text-12-medium">{command.keybind("review.toggle")}</span>
</div>
}
>
<Button variant="ghost" class="group/review-toggle size-6 p-0" onClick={layout.review.toggle}>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
name={layout.review.opened() ? "layout-right" : "layout-left"}
size="small"
class="group-hover/review-toggle:hidden"
/>
<Icon
name={layout.review.opened() ? "layout-right-partial" : "layout-left-partial"}
size="small"
class="hidden group-hover/review-toggle:inline-block"
/>
<Icon
name={layout.review.opened() ? "layout-right-full" : "layout-left-full"}
size="small"
class="hidden group-active/review-toggle:inline-block"
/>
</div>
</Button>
</Tooltip>
</Show>
<Tooltip
class="hidden md:block shrink-0"
value={
<div class="flex items-center gap-2">
<span>Toggle terminal</span>
<span class="text-icon-base text-12-medium">{command.keybind("terminal.toggle")}</span>
</div>
}
>
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
class="group-hover/terminal-toggle:hidden"
/>
<Icon
size="small"
name="layout-bottom-partial"
class="hidden group-hover/terminal-toggle:inline-block"
/>
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
class="hidden group-active/terminal-toggle:inline-block"
/>
</div>
</Button>
</Tooltip>
</div>
<Show when={shareEnabled() && currentSession()}>
<Popover
title="Share session"
trigger={
<Tooltip class="shrink-0" value="Share session">
<IconButton icon="share" variant="ghost" class="" />
</Tooltip>
}
>
{iife(() => {
const [url] = createResource(
() => currentSession(),
async (session) => {
if (!session) return
let shareURL = session.share?.url
if (!shareURL) {
shareURL = await globalSDK.client.session
.share({ sessionID: session.id, directory: sync.directory })
.then((r) => r.data?.share?.url)
.catch((e) => {
console.error("Failed to share session", e)
return undefined
})
}
return shareURL
},
)
return <Show when={url()}>{(url) => <TextField value={url()} readOnly copyable class="w-72" />}</Show>
})}
</Popover>
</Show>
</div>
</div>
</header>
)
}
export default function Page() {
const layout = useLayout()
const local = useLocal()
@@ -718,6 +933,7 @@ export default function Page() {
return (
<div class="relative bg-background-base size-full overflow-hidden flex flex-col">
<Header />
<div class="md:hidden flex-1 min-h-0 flex flex-col bg-background-stronger">
<Switch>
<Match when={!params.id}>
@@ -742,6 +958,8 @@ export default function Page() {
<div class="relative h-full mt-6 overflow-y-auto no-scrollbar">
<SessionReview
diffs={diffs()}
diffStyle={layout.review.diffStyle()}
onDiffStyleChange={layout.review.setDiffStyle}
classes={{
root: "pb-32",
header: "px-4",
@@ -867,7 +1085,8 @@ export default function Page() {
container: "px-6",
}}
diffs={diffs()}
split
diffStyle={layout.review.diffStyle()}
onDiffStyleChange={layout.review.setDiffStyle}
/>
</div>
</Tabs.Content>
@@ -999,10 +1218,6 @@ export default function Page() {
</DragDropProvider>
</div>
</Show>
<StatusBar>
<SessionLspIndicator />
<SessionMcpIndicator />
</StatusBar>
</div>
)
}

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.0.218",
"version": "1.0.220",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",

View File

@@ -1,5 +1,5 @@
import { json, action, useParams, createAsync, useSubmission } from "@solidjs/router"
import { createEffect, Show } from "solid-js"
import { createEffect, Show, createMemo } from "solid-js"
import { createStore } from "solid-js/store"
import { withActor } from "~/context/auth.withActor"
import { Billing } from "@opencode-ai/console-core/billing.js"
@@ -68,6 +68,12 @@ export function ReloadSection() {
reloadTrigger: "",
})
const processingFee = createMemo(() => {
const reloadAmount = billingInfo()?.reloadAmount
if (!reloadAmount) return "0.00"
return (((reloadAmount + 0.3) / 0.956) * 0.044 + 0.3).toFixed(2)
})
createEffect(() => {
if (!setReloadSubmission.pending && setReloadSubmission.result && !(setReloadSubmission.result as any).error) {
setStore("show", false)
@@ -104,8 +110,8 @@ export function ReloadSection() {
}
>
<p>
Auto reload is <b>enabled</b>. We'll reload <b>${billingInfo()?.reloadAmount}</b> (+$1.23 processing fee)
when balance reaches <b>${billingInfo()?.reloadTrigger}</b>.
Auto reload is <b>enabled</b>. We'll reload <b>${billingInfo()?.reloadAmount}</b> (+${processingFee()}{" "}
processing fee) when balance reaches <b>${billingInfo()?.reloadTrigger}</b>.
</p>
</Show>
<button data-color="primary" type="button" onClick={() => show()}>

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.0.218",
"version": "1.0.220",
"private": true,
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.0.218",
"version": "1.0.220",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.0.218",
"version": "1.0.220",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",

View File

@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.0.218",
"version": "1.0.220",
"type": "module",
"scripts": {
"typecheck": "tsgo -b",

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.0.218",
"version": "1.0.220",
"private": true,
"type": "module",
"scripts": {

View File

@@ -33,7 +33,7 @@ const ClientOnlyCode = clientOnly(() => import("@opencode-ai/ui/code").then((m)
const ClientOnlyWorkerPoolProvider = clientOnly(() =>
import("@opencode-ai/ui/pierre/worker").then((m) => ({
default: (props: { children: any }) => (
<WorkerPoolProvider pool={m.workerPool}>{props.children}</WorkerPoolProvider>
<WorkerPoolProvider pools={m.getWorkerPools()}>{props.children}</WorkerPoolProvider>
),
})),
)

View File

@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.0.218"
version = "1.0.220"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/sst/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.218/opencode-darwin-arm64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.220/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.218/opencode-darwin-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.220/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.218/opencode-linux-arm64.tar.gz"
archive = "https://github.com/sst/opencode/releases/download/v1.0.220/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.218/opencode-linux-x64.tar.gz"
archive = "https://github.com/sst/opencode/releases/download/v1.0.220/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.218/opencode-windows-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.220/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.0.218",
"version": "1.0.220",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.0.218",
"version": "1.0.220",
"name": "opencode",
"type": "module",
"private": true,

View File

@@ -0,0 +1,51 @@
import { EOL } from "os"
import { basename } from "path"
import { Agent } from "../../../agent/agent"
import { Provider } from "../../../provider/provider"
import { ToolRegistry } from "../../../tool/registry"
import { Wildcard } from "../../../util/wildcard"
import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd"
export const AgentCommand = cmd({
command: "agent <name>",
builder: (yargs) =>
yargs.positional("name", {
type: "string",
demandOption: true,
description: "Agent name",
}),
async handler(args) {
await bootstrap(process.cwd(), async () => {
const agentName = args.name as string
const agent = await Agent.get(agentName)
if (!agent) {
process.stderr.write(
`Agent ${agentName} not found, run '${basename(process.execPath)} agent list' to get an agent list` + EOL,
)
process.exit(1)
}
const resolvedTools = await resolveTools(agent)
const output = {
...agent,
tools: resolvedTools,
toolOverrides: agent.tools,
}
process.stdout.write(JSON.stringify(output, null, 2) + EOL)
})
},
})
async function resolveTools(agent: Agent.Info) {
const providerID = agent.model?.providerID ?? (await Provider.defaultModel()).providerID
const toolOverrides = {
...agent.tools,
...(await ToolRegistry.enabled(agent)),
}
const availableTools = await ToolRegistry.tools(providerID, agent)
const resolved: Record<string, boolean> = {}
for (const tool of availableTools) {
resolved[tool.id] = Wildcard.all(tool.id, toolOverrides) !== false
}
return resolved
}

View File

@@ -8,6 +8,7 @@ import { RipgrepCommand } from "./ripgrep"
import { ScrapCommand } from "./scrap"
import { SkillCommand } from "./skill"
import { SnapshotCommand } from "./snapshot"
import { AgentCommand } from "./agent"
export const DebugCommand = cmd({
command: "debug",
@@ -20,6 +21,7 @@ export const DebugCommand = cmd({
.command(ScrapCommand)
.command(SkillCommand)
.command(SnapshotCommand)
.command(AgentCommand)
.command(PathsCommand)
.command({
command: "wait",

View File

@@ -259,7 +259,7 @@ export function Autocomplete(props: {
const s = session()
for (const command of sync.data.command) {
results.push({
display: "/" + command.name,
display: "/" + command.name + (command.mcp ? " (MCP)" : ""),
description: command.description,
onSelect: () => {
const newText = "/" + command.name + " "

View File

@@ -28,7 +28,7 @@ export const TIPS = [
"Press {highlight}Ctrl+C{/highlight} when typing to clear the input field.",
"Press {highlight}Escape{/highlight} to stop the AI mid-response.",
"Switch to {highlight}Plan{/highlight} agent to get suggestions without making actual changes.",
"Use {highlight}@agent-name{/highlight} in prompts to invoke specialized subagents.",
"Use {highlight}@<agent-name>{/highlight} in prompts to invoke specialized subagents.",
"Press {highlight}Ctrl+X Right/Left{/highlight} to cycle through parent and child sessions.",
"Create {highlight}opencode.json{/highlight} in project root for project-specific settings.",
"Place settings in {highlight}~/.config/opencode/opencode.json{/highlight} for global config.",

View File

@@ -17,6 +17,12 @@ const options = {
describe: "enable mDNS service discovery (defaults hostname to 0.0.0.0)",
default: false,
},
cors: {
type: "string" as const,
array: true,
describe: "additional domains to allow for CORS",
default: [] as string[],
},
}
export type NetworkOptions = InferredOptionTypes<typeof options>
@@ -30,6 +36,7 @@ export async function resolveNetworkOptions(args: NetworkOptions) {
const portExplicitlySet = process.argv.includes("--port")
const hostnameExplicitlySet = process.argv.includes("--hostname")
const mdnsExplicitlySet = process.argv.includes("--mdns")
const corsExplicitlySet = process.argv.includes("--cors")
const mdns = mdnsExplicitlySet ? args.mdns : (config?.server?.mdns ?? args.mdns)
const port = portExplicitlySet ? args.port : (config?.server?.port ?? args.port)
@@ -38,6 +45,9 @@ export async function resolveNetworkOptions(args: NetworkOptions) {
: mdns && !config?.server?.hostname
? "0.0.0.0"
: (config?.server?.hostname ?? args.hostname)
const configCors = config?.server?.cors ?? []
const argsCors = Array.isArray(args.cors) ? args.cors : args.cors ? [args.cors] : []
const cors = [...configCors, ...argsCors]
return { hostname, port, mdns }
return { hostname, port, mdns, cors }
}

View File

@@ -1,4 +1,3 @@
import { Bus } from "@/bus"
import { BusEvent } from "@/bus/bus-event"
import z from "zod"
import { Config } from "../config/config"
@@ -27,6 +26,7 @@ export namespace Command {
description: z.string().optional(),
agent: z.string().optional(),
model: z.string().optional(),
mcp: z.boolean().optional(),
// workaround for zod not supporting async functions natively so we use getters
// https://zod.dev/v4/changelog?id=zfunction
template: z.promise(z.string()).or(z.string()),
@@ -94,6 +94,7 @@ export namespace Command {
for (const [name, prompt] of Object.entries(await MCP.prompts())) {
result[name] = {
name,
mcp: true,
description: prompt.description,
get template() {
// since a getter can't be async we need to manually return a promise here

View File

@@ -259,7 +259,7 @@ export namespace Config {
return result
}
const MODE_GLOB = new Bun.Glob("{mode,modes}/**/*.md")
const MODE_GLOB = new Bun.Glob("{mode,modes}/*.md")
async function loadMode(dir: string) {
const result: Record<string, Agent> = {}
for await (const item of MODE_GLOB.scan({
@@ -288,7 +288,7 @@ export namespace Config {
return result
}
const PLUGIN_GLOB = new Bun.Glob("{plugin,plugins}/**/*.{ts,js}")
const PLUGIN_GLOB = new Bun.Glob("{plugin,plugins}/*.{ts,js}")
async function loadPlugin(dir: string) {
const plugins: string[] = []
@@ -586,6 +586,7 @@ export namespace Config {
port: z.number().int().positive().optional().describe("Port to listen on"),
hostname: z.string().optional().describe("Hostname to listen on"),
mdns: z.boolean().optional().describe("Enable mDNS service discovery"),
cors: z.array(z.string()).optional().describe("Additional domains to allow for CORS"),
})
.strict()
.meta({

View File

@@ -5,8 +5,6 @@ import fs from "fs/promises"
import z from "zod"
import { NamedError } from "@opencode-ai/util/error"
import { lazy } from "../util/lazy"
import { $ } from "bun"
import { ZipReader, BlobReader, BlobWriter } from "@zip.js/zip.js"
import { Log } from "@/util/log"
@@ -368,7 +366,7 @@ export namespace Ripgrep {
}
export async function search(input: { cwd: string; pattern: string; glob?: string[]; limit?: number }) {
const args = [`${await filepath()}`, "--json", "--hidden", "--glob='!.git/*'"]
const args = [await filepath(), "--json", "--hidden", "--glob=!.git/*"]
if (input.glob) {
for (const g of input.glob) {
@@ -383,15 +381,21 @@ export namespace Ripgrep {
args.push("--")
args.push(input.pattern)
const command = args.join(" ")
const result = await $`${{ raw: command }}`.cwd(input.cwd).quiet().nothrow()
if (result.exitCode !== 0) {
const proc = Bun.spawn(args, {
cwd: input.cwd,
stdout: "pipe",
stderr: "ignore",
})
const output = await Bun.readableStreamToText(proc.stdout)
await proc.exited
if (proc.exitCode !== 0) {
return []
}
// Handle both Unix (\n) and Windows (\r\n) line endings
const lines = result.text().trim().split(/\r?\n/).filter(Boolean)
// Parse JSON lines from ripgrep output
const lines = output.trim().split(/\r?\n/).filter(Boolean)
return lines
.map((line) => JSON.parse(line))

View File

@@ -98,6 +98,9 @@ export namespace LSPClient {
},
workspace: {
configuration: true,
didChangeWatchedFiles: {
dynamicRegistration: true,
},
},
textDocument: {
synchronization: {
@@ -151,6 +154,16 @@ export namespace LSPClient {
const version = files[input.path]
if (version !== undefined) {
log.info("workspace/didChangeWatchedFiles", input)
await connection.sendNotification("workspace/didChangeWatchedFiles", {
changes: [
{
uri: pathToFileURL(input.path).href,
type: 2, // Changed
},
],
})
const next = version + 1
files[input.path] = next
log.info("textDocument/didChange", {
@@ -167,6 +180,16 @@ export namespace LSPClient {
return
}
log.info("workspace/didChangeWatchedFiles", input)
await connection.sendNotification("workspace/didChangeWatchedFiles", {
changes: [
{
uri: pathToFileURL(input.path).href,
type: 1, // Created
},
],
})
log.info("textDocument/didOpen", input)
diagnostics.delete(input.path)
await connection.sendNotification("textDocument/didOpen", {

View File

@@ -195,7 +195,7 @@ export namespace MCP {
for (const prompt of prompts.prompts) {
const sanitizedClientName = clientName.replace(/[^a-zA-Z0-9_-]/g, "_")
const sanitizedPromptName = prompt.name.replace(/[^a-zA-Z0-9_-]/g, "_")
const key = sanitizedClientName + ":" + sanitizedPromptName + " (MCP)"
const key = sanitizedClientName + ":" + sanitizedPromptName
commands[key] = { ...prompt, client: clientName }
}

View File

@@ -45,7 +45,6 @@ import { Snapshot } from "@/snapshot"
import { SessionSummary } from "@/session/summary"
import { SessionStatus } from "@/session/status"
import { upgradeWebSocket, websocket } from "hono/bun"
import type { BunWebSocketData } from "hono/bun"
import { errors } from "./error"
import { Pty } from "@/pty"
import { Installation } from "@/installation"
@@ -58,6 +57,7 @@ export namespace Server {
const log = Log.create({ service: "server" })
let _url: URL | undefined
let _corsWhitelist: string[] = []
export function url(): URL {
return _url ?? new URL("http://localhost:4096")
@@ -117,6 +117,10 @@ export namespace Server {
if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) {
return input
}
if (_corsWhitelist.includes(input)) {
return input
}
return
},
}),
@@ -2676,7 +2680,9 @@ export namespace Server {
return result
}
export function listen(opts: { port: number; hostname: string; mdns?: boolean }) {
export function listen(opts: { port: number; hostname: string; mdns?: boolean; cors?: string[] }) {
_corsWhitelist = opts.cors ?? []
const args = {
hostname: opts.hostname,
idleTimeout: 0,
@@ -2702,7 +2708,7 @@ export namespace Server {
opts.hostname !== "localhost" &&
opts.hostname !== "::1"
if (shouldPublishMDNS) {
MDNS.publish(server.port!)
MDNS.publish(server.port!, `opencode-${server.port!}`)
} else if (opts.mdns) {
log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish")
}

View File

@@ -0,0 +1,331 @@
import { describe, expect, test } from "bun:test"
import path from "path"
import { Ripgrep } from "../../src/file/ripgrep"
import { tmpdir } from "../fixture/fixture"
describe("Ripgrep.tree", () => {
test("generates tree for flat directory", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, "a.ts"), "content")
await Bun.write(path.join(dir, "b.ts"), "content")
await Bun.write(path.join(dir, "c.ts"), "content")
},
})
const result = await Ripgrep.tree({ cwd: tmp.path })
const lines = result.split("\n")
expect(lines).toContain("a.ts")
expect(lines).toContain("b.ts")
expect(lines).toContain("c.ts")
})
test("generates tree with nested directories", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, "src", "index.ts"), "content")
await Bun.write(path.join(dir, "src", "utils", "helper.ts"), "content")
await Bun.write(path.join(dir, "README.md"), "content")
},
})
const result = await Ripgrep.tree({ cwd: tmp.path })
expect(result).toContain("src/")
expect(result).toContain("index.ts")
expect(result).toContain("utils/")
expect(result).toContain("helper.ts")
expect(result).toContain("README.md")
})
test("sorts directories before files", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, "aaa.txt"), "content")
await Bun.write(path.join(dir, "zzz", "file.txt"), "content")
},
})
const result = await Ripgrep.tree({ cwd: tmp.path })
const lines = result.split("\n")
const dirIndex = lines.findIndex((l) => l.includes("zzz/"))
const fileIndex = lines.findIndex((l) => l.includes("aaa.txt"))
expect(dirIndex).toBeLessThan(fileIndex)
})
test("sorts alphabetically within same type", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, "c.txt"), "content")
await Bun.write(path.join(dir, "a.txt"), "content")
await Bun.write(path.join(dir, "b.txt"), "content")
},
})
const result = await Ripgrep.tree({ cwd: tmp.path })
const lines = result.split("\n").filter(Boolean)
const aIndex = lines.findIndex((l) => l.includes("a.txt"))
const bIndex = lines.findIndex((l) => l.includes("b.txt"))
const cIndex = lines.findIndex((l) => l.includes("c.txt"))
expect(aIndex).toBeLessThan(bIndex)
expect(bIndex).toBeLessThan(cIndex)
})
test("respects limit parameter", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
for (let i = 0; i < 100; i++) {
await Bun.write(path.join(dir, `file${i.toString().padStart(3, "0")}.txt`), "content")
}
},
})
const result = await Ripgrep.tree({ cwd: tmp.path, limit: 10 })
expect(result).toContain("[")
expect(result).toContain("truncated]")
})
test("excludes .opencode directory", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, "src", "index.ts"), "content")
await Bun.write(path.join(dir, ".opencode", "config.json"), "content")
},
})
const result = await Ripgrep.tree({ cwd: tmp.path })
expect(result).not.toContain(".opencode")
expect(result).toContain("src/")
})
test("handles empty directory", async () => {
await using tmp = await tmpdir({ git: true })
const result = await Ripgrep.tree({ cwd: tmp.path })
expect(result).toBe("")
})
test("indents nested items correctly", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, "a", "b", "c.txt"), "content")
},
})
const result = await Ripgrep.tree({ cwd: tmp.path })
const lines = result.split("\n")
const aLine = lines.find((l) => l.includes("a/"))
const bLine = lines.find((l) => l.includes("b/"))
const cLine = lines.find((l) => l.includes("c.txt"))
expect(aLine).toBe("a/")
expect(bLine).toBe("\tb/")
expect(cLine).toBe("\t\tc.txt")
})
test("default limit is 50", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
for (let i = 0; i < 60; i++) {
await Bun.write(path.join(dir, `file${i.toString().padStart(3, "0")}.txt`), "content")
}
},
})
const result = await Ripgrep.tree({ cwd: tmp.path })
expect(result).toContain("truncated]")
})
})
describe("Ripgrep.files", () => {
test("lists files in directory", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, "file1.ts"), "content")
await Bun.write(path.join(dir, "file2.ts"), "content")
},
})
const files = await Array.fromAsync(Ripgrep.files({ cwd: tmp.path }))
expect(files).toContain("file1.ts")
expect(files).toContain("file2.ts")
})
test("respects glob filter", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, "file1.ts"), "content")
await Bun.write(path.join(dir, "file2.js"), "content")
},
})
const files = await Array.fromAsync(Ripgrep.files({ cwd: tmp.path, glob: ["*.ts"] }))
expect(files).toContain("file1.ts")
expect(files).not.toContain("file2.js")
})
test("includes hidden files by default", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, ".hidden"), "content")
await Bun.write(path.join(dir, "visible"), "content")
},
})
const files = await Array.fromAsync(Ripgrep.files({ cwd: tmp.path }))
expect(files).toContain(".hidden")
expect(files).toContain("visible")
})
test("respects maxDepth option", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, "top.txt"), "content")
await Bun.write(path.join(dir, "level1", "mid.txt"), "content")
await Bun.write(path.join(dir, "level1", "level2", "deep.txt"), "content")
},
})
const files = await Array.fromAsync(Ripgrep.files({ cwd: tmp.path, maxDepth: 1 }))
expect(files).toContain("top.txt")
expect(files).not.toContain(path.join("level1", "mid.txt"))
})
test("throws for non-existent directory", async () => {
const nonexistent = "/tmp/nonexistent-dir-" + Math.random().toString(36).slice(2)
expect(Array.fromAsync(Ripgrep.files({ cwd: nonexistent }))).rejects.toThrow()
})
test("excludes .git directory by default", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, "src.ts"), "content")
},
})
const files = await Array.fromAsync(Ripgrep.files({ cwd: tmp.path }))
const gitFiles = files.filter((f) => f.includes(".git"))
expect(gitFiles.length).toBe(0)
expect(files).toContain("src.ts")
})
test("respects exclude glob pattern", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, "keep.ts"), "content")
await Bun.write(path.join(dir, "ignore.test.ts"), "content")
},
})
const files = await Array.fromAsync(Ripgrep.files({ cwd: tmp.path, glob: ["!*.test.ts"] }))
expect(files).toContain("keep.ts")
expect(files).not.toContain("ignore.test.ts")
})
})
describe("Ripgrep.search", () => {
test("finds matches in files", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, "test.ts"), "function hello() { return 'world' }")
},
})
const results = await Ripgrep.search({ cwd: tmp.path, pattern: "hello" })
expect(results.length).toBeGreaterThan(0)
expect(results[0].path.text).toBe("test.ts")
expect(results[0].lines.text).toContain("hello")
})
test("returns empty array for no matches", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, "test.ts"), "function hello() {}")
},
})
const results = await Ripgrep.search({ cwd: tmp.path, pattern: "nonexistentpattern123" })
expect(results).toEqual([])
})
test("respects limit parameter", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
const content = "match\nmatch\nmatch\nmatch\nmatch"
await Bun.write(path.join(dir, "test.txt"), content)
},
})
const results = await Ripgrep.search({ cwd: tmp.path, pattern: "match", limit: 2 })
expect(results.length).toBe(2)
})
test("includes line numbers", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, "test.txt"), "line1\nline2\nmatch here\nline4")
},
})
const results = await Ripgrep.search({ cwd: tmp.path, pattern: "match" })
expect(results[0].line_number).toBe(3)
})
test("includes submatches", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, "test.txt"), "hello world")
},
})
const results = await Ripgrep.search({ cwd: tmp.path, pattern: "world" })
expect(results[0].submatches.length).toBeGreaterThan(0)
expect(results[0].submatches[0].match.text).toBe("world")
})
test("respects glob filter", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, "include.ts"), "searchterm")
await Bun.write(path.join(dir, "exclude.js"), "searchterm")
},
})
const results = await Ripgrep.search({ cwd: tmp.path, pattern: "searchterm", glob: ["*.ts"] })
expect(results.length).toBe(1)
expect(results[0].path.text).toBe("include.ts")
})
test("respects exclude glob filter", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, "include.ts"), "searchterm")
await Bun.write(path.join(dir, "node_modules", "exclude.ts"), "searchterm")
},
})
const results = await Ripgrep.search({ cwd: tmp.path, pattern: "searchterm", glob: ["!node_modules/**"] })
expect(results.length).toBe(1)
expect(results[0].path.text).toBe("include.ts")
})
})

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.0.218",
"version": "1.0.220",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.0.218",
"version": "1.0.220",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",

View File

@@ -1177,6 +1177,10 @@ export type ServerConfig = {
* Enable mDNS service discovery
*/
mdns?: boolean
/**
* Additional domains to allow for CORS
*/
cors?: Array<string>
}
export type AgentConfig = {
@@ -1729,6 +1733,7 @@ export type Command = {
description?: string
agent?: string
model?: string
mcp?: boolean
template: string
subtask?: boolean
hints: Array<string>

View File

@@ -7793,6 +7793,13 @@
"mdns": {
"description": "Enable mDNS service discovery",
"type": "boolean"
},
"cors": {
"description": "Additional domains to allow for CORS",
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
@@ -8975,14 +8982,30 @@
"model": {
"type": "string"
},
"mcp": {
"type": "boolean"
},
"template": {
"type": "string"
"anyOf": [
{
"type": "string"
},
{
"type": "string"
}
]
},
"subtask": {
"type": "boolean"
},
"hints": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": ["name", "template"]
"required": ["name", "template", "hints"]
},
"Model": {
"type": "object",

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.0.218",
"version": "1.0.220",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.0.218",
"version": "1.0.220",
"type": "module",
"exports": {
"./*": "./src/components/*.tsx",

View File

@@ -1,7 +1,7 @@
import { type FileContents, File, FileOptions, LineAnnotation } from "@pierre/diffs"
import { ComponentProps, createEffect, createMemo, splitProps } from "solid-js"
import { createDefaultOptions, styleVariables } from "../pierre"
import { workerPool } from "../pierre/worker"
import { getWorkerPool } from "../pierre/worker"
export type CodeProps<T = {}> = FileOptions<T> & {
file: FileContents
@@ -21,7 +21,7 @@ export function Code<T>(props: CodeProps<T>) {
...createDefaultOptions<T>("unified"),
...others,
},
workerPool,
getWorkerPool("unified"),
),
)

View File

@@ -13,7 +13,7 @@ export function Diff<T>(props: SSRDiffProps<T>) {
let container!: HTMLDivElement
let fileDiffRef!: HTMLElement
const [local, others] = splitProps(props, ["before", "after", "class", "classList", "annotations"])
const workerPool = useWorkerPool()
const workerPool = useWorkerPool(props.diffStyle)
let fileDiffInstance: FileDiff<T> | undefined
const cleanupFunctions: Array<() => void> = []

View File

@@ -1,7 +1,7 @@
import { FileDiff } from "@pierre/diffs"
import { createEffect, createMemo, onCleanup, splitProps } from "solid-js"
import { createDefaultOptions, type DiffProps, styleVariables } from "../pierre"
import { workerPool } from "../pierre/worker"
import { getWorkerPool } from "../pierre/worker"
// interface ThreadMetadata {
// threadId: string
@@ -20,26 +20,23 @@ export function Diff<T>(props: DiffProps<T>) {
...createDefaultOptions(props.diffStyle),
...others,
},
workerPool,
getWorkerPool(props.diffStyle),
),
)
const cleanupFunctions: Array<() => void> = []
createEffect(() => {
const diff = fileDiff()
container.innerHTML = ""
fileDiff().render({
diff.render({
oldFile: local.before,
newFile: local.after,
lineAnnotations: local.annotations,
containerWrapper: container,
})
})
onCleanup(() => {
// Clean up FileDiff event handlers and dispose SolidJS components
fileDiff()?.cleanUp()
cleanupFunctions.forEach((dispose) => dispose())
onCleanup(() => {
diff.cleanUp()
})
})
return <div data-component="diff" style={styleVariables} ref={container} />

View File

@@ -57,6 +57,7 @@ const icons = {
share: `<path d="M10.0013 12.0846L10.0013 3.33464M13.7513 6.66797L10.0013 2.91797L6.2513 6.66797M17.0846 10.418V17.0846H2.91797V10.418" stroke="currentColor" stroke-linecap="square"/>`,
download: `<path d="M13.9583 10.6257L10 14.584L6.04167 10.6257M10 2.08398V13.959M16.25 17.9173H3.75" stroke="currentColor" stroke-linecap="square"/>`,
menu: `<path d="M2.5 5H17.5M2.5 10H17.5M2.5 15H17.5" stroke="currentColor" stroke-linecap="square"/>`,
server: `<rect x="3.35547" y="1.92969" width="13.2857" height="16.1429" stroke="currentColor"/><rect x="3.35547" y="11.9297" width="13.2857" height="6.14286" stroke="currentColor"/><rect x="12.8555" y="14.2852" width="1.42857" height="1.42857" fill="currentColor"/><rect x="10" y="14.2852" width="1.42857" height="1.42857" fill="currentColor"/>`,
}
export interface IconProps extends ComponentProps<"svg"> {

View File

@@ -78,10 +78,9 @@
[data-slot="user-message-text"] {
white-space: pre-wrap;
overflow: hidden;
background: var(--surface-inset-base);
padding: 6px 12px;
background: var(--surface-base);
padding: 8px 12px;
border-radius: 4px;
border: 0.5px solid var(--border-weak-base);
}
.text-text-strong {

View File

@@ -874,7 +874,6 @@ ToolRegistry.register({
return (
<BasicTool
{...props}
defaultOpen
icon="code-lines"
trigger={
<div data-component="edit-trigger">
@@ -926,7 +925,6 @@ ToolRegistry.register({
return (
<BasicTool
{...props}
defaultOpen
icon="code-lines"
trigger={
<div data-component="write-trigger">

View File

@@ -7,7 +7,7 @@
all: unset;
background-color: var(--surface-base);
border-radius: var(--radius-md);
box-shadow: inset 0 0 0 1px var(--border-weak-base);
box-shadow: var(--shadow-xs-border);
margin: 0;
padding: 0;
position: relative;
@@ -23,10 +23,7 @@
[data-slot="radio-group-indicator"] {
background: var(--button-secondary-base);
border-radius: var(--radius-md);
box-shadow:
var(--shadow-xs),
inset 0 0 0 var(--indicator-focus-width, 0px) var(--border-selected),
inset 0 0 0 1px var(--border-base);
box-shadow: var(--shadow-xs-border);
content: "";
opacity: var(--indicator-opacity, 1);
position: absolute;
@@ -115,7 +112,7 @@
/* Focus state */
[data-slot="radio-group-wrapper"]:has([data-slot="radio-group-item-input"]:focus-visible)
[data-slot="radio-group-indicator"] {
--indicator-focus-width: 2px;
box-shadow: var(--shadow-xs-border-focus);
}
/* Hide indicator when nothing is checked */

View File

@@ -1,5 +1,6 @@
import { Accordion } from "./accordion"
import { Button } from "./button"
import { RadioGroup } from "./radio-group"
import { DiffChanges } from "./diff-changes"
import { FileIcon } from "./file-icon"
import { Icon } from "./icon"
@@ -13,8 +14,12 @@ import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
import { Dynamic } from "solid-js/web"
import { checksum } from "@opencode-ai/util/encode"
export type SessionReviewDiffStyle = "unified" | "split"
export interface SessionReviewProps {
split?: boolean
diffStyle?: SessionReviewDiffStyle
onDiffStyleChange?: (diffStyle: SessionReviewDiffStyle) => void
class?: string
classList?: Record<string, boolean | undefined>
classes?: { root?: string; header?: string; container?: string }
@@ -28,6 +33,8 @@ export const SessionReview = (props: SessionReviewProps) => {
open: props.diffs.length > 10 ? [] : props.diffs.map((d) => d.file),
})
const diffStyle = () => props.diffStyle ?? (props.split ? "split" : "unified")
const handleChange = (open: string[]) => {
setStore("open", open)
}
@@ -60,6 +67,15 @@ export const SessionReview = (props: SessionReviewProps) => {
>
<div data-slot="session-review-title">Session changes</div>
<div data-slot="session-review-actions">
<Show when={props.onDiffStyleChange}>
<RadioGroup
options={["unified", "split"] as const}
current={diffStyle()}
value={(style) => style}
label={(style) => (style === "unified" ? "Unified" : "Split")}
onSelect={(style) => style && props.onDiffStyleChange?.(style)}
/>
</Show>
<Button size="normal" icon="chevron-grabber-vertical" onClick={handleExpandOrCollapseAll}>
<Switch>
<Match when={store.open.length > 0}>Collapse all</Match>
@@ -102,7 +118,7 @@ export const SessionReview = (props: SessionReviewProps) => {
<Dynamic
component={diffComponent}
preloadedDiff={diff.preloaded}
diffStyle={props.split ? "split" : "unified"}
diffStyle={diffStyle()}
before={{
name: diff.file!,
contents: diff.before!,

View File

@@ -1,10 +1,20 @@
import type { WorkerPoolManager } from "@pierre/diffs/worker"
import { createSimpleContext } from "./helper"
const ctx = createSimpleContext<WorkerPoolManager | undefined, { pool: WorkerPoolManager | undefined }>({
export type WorkerPools = {
unified: WorkerPoolManager | undefined
split: WorkerPoolManager | undefined
}
const ctx = createSimpleContext<WorkerPools, { pools: WorkerPools }>({
name: "WorkerPool",
init: (props) => props.pool,
init: (props) => props.pools,
})
export const WorkerPoolProvider = ctx.provider
export const useWorkerPool = ctx.use
export function useWorkerPool(diffStyle: "unified" | "split" | undefined) {
const pools = ctx.use()
if (diffStyle === "split") return pools.split
return pools.unified
}

View File

@@ -1,16 +1,15 @@
import { getOrCreateWorkerPoolSingleton, WorkerPoolManager } from "@pierre/diffs/worker"
import { WorkerPoolManager } from "@pierre/diffs/worker"
import ShikiWorkerUrl from "@pierre/diffs/worker/worker.js?worker&url"
export type WorkerPoolStyle = "unified" | "split"
export function workerFactory(): Worker {
return new Worker(ShikiWorkerUrl, { type: "module" })
}
export const workerPool: WorkerPoolManager | undefined = (() => {
if (typeof window === "undefined") {
return undefined
}
return getOrCreateWorkerPoolSingleton({
poolOptions: {
function createPool(lineDiffType: "none" | "word-alt") {
const pool = new WorkerPoolManager(
{
workerFactory,
// poolSize defaults to 8. More workers = more parallelism but
// also more memory. Too many can actually slow things down.
@@ -19,10 +18,34 @@ export const workerPool: WorkerPoolManager | undefined = (() => {
// boot up time for workers
poolSize: 2,
},
highlighterOptions: {
{
theme: "OpenCode",
// Optionally preload languages to avoid lazy-loading delays
// langs: ["typescript", "javascript", "css", "html"],
lineDiffType,
},
})
})()
)
pool.initialize()
return pool
}
let unified: WorkerPoolManager | undefined
let split: WorkerPoolManager | undefined
export function getWorkerPool(style: WorkerPoolStyle | undefined): WorkerPoolManager | undefined {
if (typeof window === "undefined") return
if (style === "split") {
if (!split) split = createPool("word-alt")
return split
}
if (!unified) unified = createPool("none")
return unified
}
export function getWorkerPools() {
return {
unified: getWorkerPool("unified"),
split: getWorkerPool("split"),
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/util",
"version": "1.0.218",
"version": "1.0.220",
"private": true,
"type": "module",
"exports": {

View File

@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/web",
"type": "module",
"version": "1.0.218",
"version": "1.0.220",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",

View File

@@ -362,11 +362,12 @@ This starts an HTTP server that provides API access to opencode functionality wi
#### Flags
| Flag | Description |
| ------------ | --------------------- |
| `--port` | Port to listen on |
| `--hostname` | Hostname to listen on |
| `--mdns` | Enable mDNS discovery |
| Flag | Description |
| ------------ | ------------------------------------------ |
| `--port` | Port to listen on |
| `--hostname` | Hostname to listen on |
| `--mdns` | Enable mDNS discovery |
| `--cors` | Additional browser origin(s) to allow CORS |
---
@@ -457,11 +458,12 @@ This starts an HTTP server and opens a web browser to access OpenCode through a
#### Flags
| Flag | Description |
| ------------ | --------------------- |
| `--port` | Port to listen on |
| `--hostname` | Hostname to listen on |
| `--mdns` | Enable mDNS discovery |
| Flag | Description |
| ------------ | ------------------------------------------ |
| `--port` | Port to listen on |
| `--hostname` | Hostname to listen on |
| `--mdns` | Enable mDNS discovery |
| `--cors` | Additional browser origin(s) to allow CORS |
---

View File

@@ -132,7 +132,8 @@ You can configure server settings for the `opencode serve` and `opencode web` co
"server": {
"port": 4096,
"hostname": "0.0.0.0",
"mdns": true
"mdns": true,
"cors": ["http://localhost:5173"]
}
}
```
@@ -142,6 +143,7 @@ Available options:
- `port` - Port to listen on.
- `hostname` - Hostname to listen on. When `mdns` is enabled and no hostname is set, defaults to `0.0.0.0`.
- `mdns` - Enable mDNS service discovery. This allows other devices on the network to discover your OpenCode server.
- `cors` - Additional origins to allow for CORS when using the HTTP server from a browser-based client. Values must be full origins (scheme + host + optional port), eg `https://app.example.com`.
[Learn more about the server here](/docs/server).

View File

@@ -15,28 +15,29 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw
## Plugins
| Name | Description |
| ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Automatically inject Helicone session headers for request grouping |
| [opencode-skills](https://github.com/malhashemi/opencode-skills) | Manage and organize OpenCode skills and capabilities |
| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Auto-inject TypeScript/Svelte types into file reads with lookup tools |
| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Use your ChatGPT Plus/Pro subscription instead of API credits |
| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Use your existing Gemini plan instead of API billing |
| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Use Antigravity's free models instead of API billing |
| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth Plugin, with support for Google Search, and more robust API handling |
| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimize token usage by pruning obsolete tool outputs |
| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Add native websearch support for supported providers with Google grounded style |
| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Enables AI agents to run background processes in a PTY, send interactive input to them. |
| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instructions for non-interactive shell commands - prevents hangs from TTY-dependent operations |
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Track OpenCode usage with Wakatime |
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Clean up markdown tables produced by LLMs |
| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x faster code editing with Morph Fast Apply API and lazy edit markers |
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Background agents, pre-built LSP/AST/MCP tools, curated agents, Claude Code compatible |
| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Desktop notifications and sound alerts for OpenCode sessions |
| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Desktop notifications and sound alerts for permission, completion, and error events |
| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | AI-powered automatic Zellij session naming based on OpenCode context |
| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Allow OpenCode agents to lazy load prompts on demand with skill discovery and injection |
| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Persistent memory across sessions using Supermemory |
| Name | Description |
| -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Automatically inject Helicone session headers for request grouping |
| [opencode-skills](https://github.com/malhashemi/opencode-skills) | Manage and organize OpenCode skills and capabilities |
| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Auto-inject TypeScript/Svelte types into file reads with lookup tools |
| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Use your ChatGPT Plus/Pro subscription instead of API credits |
| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Use your existing Gemini plan instead of API billing |
| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Use Antigravity's free models instead of API billing |
| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth Plugin, with support for Google Search, and more robust API handling |
| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimize token usage by pruning obsolete tool outputs |
| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Add native websearch support for supported providers with Google grounded style |
| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Enables AI agents to run background processes in a PTY, send interactive input to them. |
| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instructions for non-interactive shell commands - prevents hangs from TTY-dependent operations |
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Track OpenCode usage with Wakatime |
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Clean up markdown tables produced by LLMs |
| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x faster code editing with Morph Fast Apply API and lazy edit markers |
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Background agents, pre-built LSP/AST/MCP tools, curated agents, Claude Code compatible |
| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Desktop notifications and sound alerts for OpenCode sessions |
| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Desktop notifications and sound alerts for permission, completion, and error events |
| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | AI-powered automatic Zellij session naming based on OpenCode context |
| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Allow OpenCode agents to lazy load prompts on demand with skill discovery and injection |
| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Persistent memory across sessions using Supermemory |
| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Interactive plan review with visual annotation and private/offline sharing |
---

View File

@@ -13,16 +13,23 @@ The `opencode serve` command runs a headless HTTP server that exposes an OpenAPI
### Usage
```bash
opencode serve [--port <number>] [--hostname <string>]
opencode serve [--port <number>] [--hostname <string>] [--cors <origin>]
```
#### Options
| Flag | Description | Default |
| ------------ | --------------------- | ----------- |
| `--port` | Port to listen on | `4096` |
| `--hostname` | Hostname to listen on | `127.0.0.1` |
| `--mdns` | Enable mDNS discovery | `false` |
| Flag | Description | Default |
| ------------ | ----------------------------------- | ----------- |
| `--port` | Port to listen on | `4096` |
| `--hostname` | Hostname to listen on | `127.0.0.1` |
| `--mdns` | Enable mDNS discovery | `false` |
| `--cors` | Additional browser origins to allow | `[]` |
`--cors` can be passed multiple times:
```bash
opencode serve --cors http://localhost:5173 --cors https://app.example.com
```
---

View File

@@ -64,7 +64,7 @@ export async function getCommits(from: string, to: string): Promise<Commit[]> {
const areas = new Set<string>()
for (const file of files.split("\n").filter(Boolean)) {
if (file.startsWith("packages/opencode/src/cli/cmd/tui/")) areas.add("tui")
if (file.startsWith("packages/opencode/src/cli/cmd/")) areas.add("tui")
else if (file.startsWith("packages/opencode/")) areas.add("core")
else if (file.startsWith("packages/desktop/src-tauri/")) areas.add("tauri")
else if (file.startsWith("packages/desktop/")) areas.add("app")

View File

@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.0.218",
"version": "1.0.220",
"publisher": "sst-dev",
"repository": {
"type": "git",