mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-10 19:04:17 +00:00
Compare commits
32 Commits
variants-d
...
ripgrep-te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a4c035486 | ||
|
|
40a303164e | ||
|
|
e7422ee782 | ||
|
|
dc3e731afe | ||
|
|
de50e7f9b9 | ||
|
|
f3db966317 | ||
|
|
decc616c80 | ||
|
|
e0450e74fb | ||
|
|
094af4dc7d | ||
|
|
2dc14718aa | ||
|
|
b97d20f252 | ||
|
|
fcfcdd95e9 | ||
|
|
c42bd492ea | ||
|
|
840fe030ab | ||
|
|
a7c4f83ca2 | ||
|
|
ed70a07201 | ||
|
|
18a5eb205f | ||
|
|
5249f04ea0 | ||
|
|
6e3ead198e | ||
|
|
4309d439f5 | ||
|
|
2ec6a21cc0 | ||
|
|
ebf5ad25c5 | ||
|
|
8aa34ab9f3 | ||
|
|
50ef866a02 | ||
|
|
3650fefe2d | ||
|
|
22091c29f1 | ||
|
|
e7e89dc5a6 | ||
|
|
34e9392bb4 | ||
|
|
05c3bc27ff | ||
|
|
b1a6333d17 | ||
|
|
5c9d619620 | ||
|
|
dfb9caa2a9 |
1
STATS.md
1
STATS.md
@@ -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) |
|
||||
|
||||
30
bun.lock
30
bun.lock
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.0.218",
|
||||
"version": "1.0.220",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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 })),
|
||||
])
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.218",
|
||||
"version": "1.0.220",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
||||
@@ -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()}>
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.0.218",
|
||||
"version": "1.0.220",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo -b",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.218",
|
||||
"version": "1.0.220",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
51
packages/opencode/src/cli/cmd/debug/agent.ts
Normal file
51
packages/opencode/src/cli/cmd/debug/agent.ts
Normal 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
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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 + " "
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
331
packages/opencode/test/file/ripgrep.test.ts
Normal file
331
packages/opencode/test/file/ripgrep.test.ts
Normal 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")
|
||||
})
|
||||
})
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.218",
|
||||
"version": "1.0.220",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./*": "./src/components/*.tsx",
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -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> = []
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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"> {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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!,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.0.218",
|
||||
"version": "1.0.220",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user