Compare commits

..
19 Commits
Author SHA1 Message Date
opencode ff3b68bd36 release: v1.0.198
Update Nix Hashes / update (push) Has been cancelled
2025-12-24 18:16:46 +00:00
Dax Raad 474b6fd3d1 ci 2025-12-24 13:12:29 -05:00
GitHub Action 6145b197f3 chore: generate 2025-12-24 18:08:42 +00:00
Dax Raad 918eff9233 ci 2025-12-24 13:07:56 -05:00
opencode 987e444828 release: v1.0.197
Update Nix Hashes / update (push) Has been cancelled
2025-12-24 17:47:07 +00:00
Dax Raad 99633cb299 Revert "feat: better styling for small screens (short and/or not wide) (#5968)"
This reverts commit ac371d2987.
2025-12-24 12:38:10 -05:00
GitHub Action f822331eb8 chore: generate 2025-12-24 17:07:43 +00:00
Patrick SchielandGitHub 0f053769db docs: add infos about server debugging (#6085) 2025-12-24 11:07:12 -06:00
opencode ceeaf494c4 release: v1.0.196
Update Nix Hashes / update (push) Has been cancelled
2025-12-24 16:40:16 +00:00
Adam 126d887e57 fix(desktop): last text part streaming 2025-12-24 10:35:52 -06:00
Adam e5cfc24d6b fix(desktop): render perf 2025-12-24 10:26:49 -06:00
Jay V 7f8d659737 docs: edits 2025-12-24 11:23:51 -05:00
Jay V 4b061653f2 docs: add comprehensive CLI command documentation for agent, mcp, session, stats, and web commands 2025-12-24 11:12:09 -05:00
Jay V eeed89f985 docs: make MCP server documentation more scannable and add Sentry example 2025-12-24 10:49:48 -05:00
Adam 8ab533b616 chore: cleanup 2025-12-24 09:07:31 -06:00
Adam 09a399d8d6 fix(desktop): summary flicker 2025-12-24 09:07:31 -06:00
Adam b75575884a feat(desktop): show read tool args 2025-12-24 09:07:31 -06:00
GitHub Action 5688c9fd61 chore: generate 2025-12-24 14:56:15 +00:00
Adam 08a075df61 fix(desktop): better session navigation, hide child sessions 2025-12-24 08:55:32 -06:00
32 changed files with 721 additions and 530 deletions
+2
View File
@@ -6,6 +6,8 @@ You are an expert technical documentation writer
You are not verbose
Use a relaxed and friendly tone
The title of the page should be a word or a 2-3 word phrase
The description should be one short line, should not start with "The", should
+4 -4
View File
@@ -53,12 +53,12 @@ your debugger via that URL. Other methods can result in breakpoints being mapped
Caveats:
- `*.tsx` files won't have their breakpoints correctly mapped. This seems due to Bun currently not supporting source maps on code transformed
via `BunPlugin`s (currently necessary due to our dependency on `@opentui/solid`). Currently, the best you can do in terms of debugging `*.tsx`
files is writing a `debugger;` statement. Debugging facilities like stepping won't work, but at least you will be informed if a specific code
is triggered.
- If you want to run the OpenCode TUI and have breakpoints triggered in the server code, you might need to run `bun dev spawn` instead of
the usual `bun dev`. This is because `bun dev` runs the server in a worker thread and breakpoints might not work there.
- If `spawn` does not work for you, you can debug the server separately:
- Debug server: `bun run --inspect=ws://localhost:6499/ ./src/index.ts serve --port 4096`,
then attach TUI with `opencode attach http://localhost:4096`
- Debug TUI: `bun run --inspect=ws://localhost:6499/ --conditions=browser ./src/index.ts`
Other tips and tricks:
+15 -15
View File
@@ -29,7 +29,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.0.195",
"version": "1.0.198",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -77,7 +77,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.0.195",
"version": "1.0.198",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -105,7 +105,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.0.195",
"version": "1.0.198",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -132,7 +132,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.0.195",
"version": "1.0.198",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -156,7 +156,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.0.195",
"version": "1.0.198",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -180,7 +180,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.0.195",
"version": "1.0.198",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@solid-primitives/storage": "catalog:",
@@ -207,7 +207,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.0.195",
"version": "1.0.198",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -236,7 +236,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.0.195",
"version": "1.0.198",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -252,7 +252,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.0.195",
"version": "1.0.198",
"bin": {
"opencode": "./bin/opencode",
},
@@ -346,7 +346,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.0.195",
"version": "1.0.198",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -366,7 +366,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.0.195",
"version": "1.0.198",
"devDependencies": {
"@hey-api/openapi-ts": "0.88.1",
"@tsconfig/node22": "catalog:",
@@ -377,7 +377,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.0.195",
"version": "1.0.198",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -390,7 +390,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.0.195",
"version": "1.0.198",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -425,7 +425,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.0.195",
"version": "1.0.198",
"dependencies": {
"zod": "catalog:",
},
@@ -436,7 +436,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.0.195",
"version": "1.0.198",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.0.195",
"version": "1.0.198",
"description": "",
"type": "module",
"exports": {
+12 -62
View File
@@ -129,40 +129,18 @@ export default function Layout(props: ParentProps) {
return bUpdated - aUpdated
}
function flattenSessions(sessions: Session[]): Session[] {
const childrenMap = new Map<string, Session[]>()
for (const session of sessions) {
if (session.parentID) {
const children = childrenMap.get(session.parentID) ?? []
children.push(session)
childrenMap.set(session.parentID, children)
}
}
const result: Session[] = []
function visit(session: Session) {
result.push(session)
for (const child of childrenMap.get(session.id) ?? []) {
visit(child)
}
}
for (const session of sessions) {
if (!session.parentID) visit(session)
}
return result
}
function scrollToSession(sessionId: string) {
if (!scrollContainerRef) return
const element = scrollContainerRef.querySelector(`[data-session-id="${sessionId}"]`)
if (element) {
element.scrollIntoView({ block: "center", behavior: "smooth" })
element.scrollIntoView({ block: "nearest", behavior: "smooth" })
}
}
function projectSessions(directory: string) {
if (!directory) return []
const sessions = globalSync.child(directory)[0].session.toSorted(sortSessions)
return flattenSessions(sessions ?? [])
return (sessions ?? []).filter((s) => !s.parentID)
}
const currentSessions = createMemo(() => {
@@ -342,8 +320,11 @@ export default function Layout(props: ParentProps) {
createEffect(() => {
if (!params.dir || !params.id) return
const directory = base64Decode(params.dir)
setStore("lastSession", directory, params.id)
notification.session.markViewed(params.id)
const id = params.id
setStore("lastSession", directory, id)
notification.session.markViewed(id)
layout.projects.expand(directory)
requestAnimationFrame(() => scrollToSession(id))
})
createEffect(() => {
@@ -466,13 +447,9 @@ export default function Layout(props: ParentProps) {
session: Session
slug: string
project: LocalProject
depth?: number
childrenMap: Map<string, Session[]>
mobile?: boolean
}): JSX.Element => {
const notification = useNotification()
const depth = props.depth ?? 0
const children = createMemo(() => props.childrenMap.get(props.session.id) ?? [])
const updated = createMemo(() => DateTime.fromMillis(props.session.time.updated))
const notifications = createMemo(() => notification.session.unseen(props.session.id))
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
@@ -487,7 +464,7 @@ export default function Layout(props: ParentProps) {
data-session-id={props.session.id}
class="group/session relative w-full pr-2 py-1 rounded-md cursor-default transition-colors
hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
style={{ "padding-left": `${16 + depth * 12}px` }}
style={{ "padding-left": "16px" }}
>
<Tooltip placement={props.mobile ? "bottom" : "right"} value={props.session.title} gutter={10}>
<A
@@ -541,18 +518,6 @@ export default function Layout(props: ParentProps) {
</Tooltip>
</div>
</div>
<For each={children()}>
{(child) => (
<SessionItem
session={child}
slug={props.slug}
project={props.project}
depth={depth + 1}
childrenMap={props.childrenMap}
mobile={props.mobile}
/>
)}
</For>
</>
)
}
@@ -565,17 +530,6 @@ export default function Layout(props: ParentProps) {
const [store, setProjectStore] = globalSync.child(props.project.worktree)
const sessions = createMemo(() => store.session.toSorted(sortSessions))
const rootSessions = createMemo(() => sessions().filter((s) => !s.parentID))
const childSessionsByParent = createMemo(() => {
const map = new Map<string, Session[]>()
for (const session of sessions()) {
if (session.parentID) {
const children = map.get(session.parentID) ?? []
children.push(session)
map.set(session.parentID, children)
}
}
return map
})
const hasMoreSessions = createMemo(() => store.session.length >= store.limit)
const loadMoreSessions = async () => {
setProjectStore("limit", (limit) => limit + 5)
@@ -633,13 +587,7 @@ export default function Layout(props: ParentProps) {
<nav class="hidden @[4rem]:flex w-full flex-col gap-1.5">
<For each={rootSessions()}>
{(session) => (
<SessionItem
session={session}
slug={slug()}
project={props.project}
childrenMap={childSessionsByParent()}
mobile={props.mobile}
/>
<SessionItem session={session} slug={slug()} project={props.project} mobile={props.mobile} />
)}
</For>
<Show when={rootSessions().length === 0}>
@@ -761,7 +709,9 @@ export default function Layout(props: ParentProps) {
<DragDropSensors />
<ConstrainDragXAxis />
<div
ref={sidebarProps.mobile ? undefined : scrollContainerRef}
ref={(el) => {
if (!sidebarProps.mobile) scrollContainerRef = el
}}
class="w-full min-w-8 flex flex-col gap-2 min-h-0 overflow-y-auto no-scrollbar"
>
<SortableProvider ids={layout.projects.list().map((p) => p.worktree)}>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.0.195",
"version": "1.0.198",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.0.195",
"version": "1.0.198",
"private": true,
"type": "module",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.0.195",
"version": "1.0.198",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.0.195",
"version": "1.0.198",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.0.195",
"version": "1.0.198",
"type": "module",
"scripts": {
"typecheck": "tsgo -b",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.0.195",
"version": "1.0.198",
"private": true,
"type": "module",
"scripts": {
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.0.195"
version = "1.0.198"
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.195/opencode-darwin-arm64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.198/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.195/opencode-darwin-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.198/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.195/opencode-linux-arm64.tar.gz"
archive = "https://github.com/sst/opencode/releases/download/v1.0.198/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.195/opencode-linux-x64.tar.gz"
archive = "https://github.com/sst/opencode/releases/download/v1.0.198/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.195/opencode-windows-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.198/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.0.195",
"version": "1.0.198",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.0.195",
"version": "1.0.198",
"name": "opencode",
"type": "module",
"private": true,
@@ -16,7 +16,7 @@ import { usePromptStash } from "./stash"
import { DialogStash } from "../dialog-stash"
import { type AutocompleteRef, Autocomplete } from "./autocomplete"
import { useCommandDialog } from "../dialog-command"
import { useRenderer, useTerminalDimensions } from "@opentui/solid"
import { useRenderer } from "@opentui/solid"
import { Editor } from "@tui/util/editor"
import { useExit } from "../../context/exit"
import { Clipboard } from "../../util/clipboard"
@@ -123,9 +123,6 @@ export function Prompt(props: PromptProps) {
const stash = usePromptStash()
const command = useCommandDialog()
const renderer = useRenderer()
const dimensions = useTerminalDimensions()
const tall = createMemo(() => dimensions().height > 40)
const wide = createMemo(() => dimensions().width > 120)
const { theme, syntax } = useTheme()
function promptModelWarning() {
@@ -949,21 +946,19 @@ export function Prompt(props: PromptProps) {
cursorColor={theme.text}
syntaxStyle={syntax()}
/>
<Show when={tall()}>
<box flexDirection="row" flexShrink={0} paddingTop={1} gap={1}>
<text fg={highlight()}>
{store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "}
</text>
<Show when={store.mode === "normal"}>
<box flexDirection="row" gap={1}>
<text flexShrink={0} fg={keybind.leader ? theme.textMuted : theme.text}>
{local.model.parsed().model}
</text>
<text fg={theme.textMuted}>{local.model.parsed().provider}</text>
</box>
</Show>
</box>
</Show>
<box flexDirection="row" flexShrink={0} paddingTop={1} gap={1}>
<text fg={highlight()}>
{store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "}
</text>
<Show when={store.mode === "normal"}>
<box flexDirection="row" gap={1}>
<text flexShrink={0} fg={keybind.leader ? theme.textMuted : theme.text}>
{local.model.parsed().model}
</text>
<text fg={theme.textMuted}>{local.model.parsed().provider}</text>
</box>
</Show>
</box>
</box>
</box>
<box
@@ -993,123 +988,101 @@ export function Prompt(props: PromptProps) {
/>
</box>
<box flexDirection="row" justifyContent="space-between">
<Switch>
<Match when={status().type !== "idle"}>
<box
flexDirection="row"
gap={1}
flexGrow={1}
justifyContent={status().type === "retry" ? "space-between" : "flex-start"}
>
<box flexShrink={0} flexDirection="row" gap={1}>
{/* @ts-ignore // SpinnerOptions doesn't support marginLeft */}
<spinner marginLeft={1} color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
<box flexDirection="row" gap={1} flexShrink={0}>
{(() => {
const retry = createMemo(() => {
const s = status()
if (s.type !== "retry") return
return s
})
const message = createMemo(() => {
const r = retry()
if (!r) return
if (r.message.includes("exceeded your current quota") && r.message.includes("gemini"))
return "gemini is way too hot right now"
if (r.message.length > 80) return r.message.slice(0, 80) + "..."
return r.message
})
const isTruncated = createMemo(() => {
const r = retry()
if (!r) return false
return r.message.length > 120
})
const [seconds, setSeconds] = createSignal(0)
onMount(() => {
const timer = setInterval(() => {
const next = retry()?.next
if (next) setSeconds(Math.round((next - Date.now()) / 1000))
}, 1000)
<Show when={status().type !== "idle"} fallback={<text />}>
<box
flexDirection="row"
gap={1}
flexGrow={1}
justifyContent={status().type === "retry" ? "space-between" : "flex-start"}
>
<box flexShrink={0} flexDirection="row" gap={1}>
{/* @ts-ignore // SpinnerOptions doesn't support marginLeft */}
<spinner marginLeft={1} color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
<box flexDirection="row" gap={1} flexShrink={0}>
{(() => {
const retry = createMemo(() => {
const s = status()
if (s.type !== "retry") return
return s
})
const message = createMemo(() => {
const r = retry()
if (!r) return
if (r.message.includes("exceeded your current quota") && r.message.includes("gemini"))
return "gemini is way too hot right now"
if (r.message.length > 80) return r.message.slice(0, 80) + "..."
return r.message
})
const isTruncated = createMemo(() => {
const r = retry()
if (!r) return false
return r.message.length > 120
})
const [seconds, setSeconds] = createSignal(0)
onMount(() => {
const timer = setInterval(() => {
const next = retry()?.next
if (next) setSeconds(Math.round((next - Date.now()) / 1000))
}, 1000)
onCleanup(() => {
clearTimeout(timer)
})
onCleanup(() => {
clearInterval(timer)
})
const handleMessageClick = () => {
const r = retry()
if (!r) return
if (isTruncated()) {
DialogAlert.show(dialog, "Retry Error", r.message)
}
})
const handleMessageClick = () => {
const r = retry()
if (!r) return
if (isTruncated()) {
DialogAlert.show(dialog, "Retry Error", r.message)
}
}
const retryText = () => {
const r = retry()
if (!r) return ""
const baseMessage = message()
const truncatedHint = isTruncated() ? " (click to expand)" : ""
const retryInfo = ` [retrying ${seconds() > 0 ? `in ${seconds()}s ` : ""}attempt #${r.attempt}]`
return baseMessage + truncatedHint + retryInfo
}
const retryText = () => {
const r = retry()
if (!r) return ""
const baseMessage = message()
const truncatedHint = isTruncated() ? " (click to expand)" : ""
const retryInfo = ` [retrying ${seconds() > 0 ? `in ${seconds()}s ` : ""}attempt #${r.attempt}]`
return baseMessage + truncatedHint + retryInfo
}
return (
<Show when={retry()}>
<box onMouseUp={handleMessageClick}>
<text fg={theme.error}>{retryText()}</text>
</box>
</Show>
)
})()}
</box>
return (
<Show when={retry()}>
<box onMouseUp={handleMessageClick}>
<text fg={theme.error}>{retryText()}</text>
</box>
</Show>
)
})()}
</box>
<text fg={store.interrupt > 0 ? theme.primary : theme.text}>
esc{" "}
<span style={{ fg: store.interrupt > 0 ? theme.primary : theme.textMuted }}>
{store.interrupt > 0 ? "again to interrupt" : "interrupt"}
</span>
</text>
</box>
</Match>
<Match when={!tall()}>
<box flexDirection="row" gap={1}>
<text fg={highlight()}>
{store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "}
</text>
<Show when={store.mode === "normal"}>
<box flexDirection="row" gap={1}>
<text flexShrink={0} fg={keybind.leader ? theme.textMuted : theme.text}>
{local.model.parsed().model}
</text>
<text fg={theme.textMuted}>{local.model.parsed().provider}</text>
</box>
</Show>
</box>
</Match>
</Switch>
<box gap={2} flexDirection="row" marginLeft="auto">
<Switch>
<Match when={store.mode === "normal"}>
<Show when={wide()}>
<text fg={store.interrupt > 0 ? theme.primary : theme.text}>
esc{" "}
<span style={{ fg: store.interrupt > 0 ? theme.primary : theme.textMuted }}>
{store.interrupt > 0 ? "again to interrupt" : "interrupt"}
</span>
</text>
</box>
</Show>
<Show when={status().type !== "retry"}>
<box gap={2} flexDirection="row">
<Switch>
<Match when={store.mode === "normal"}>
<text fg={theme.text}>
{keybind.print("agent_cycle")} <span style={{ fg: theme.textMuted }}>switch agent</span>
</text>
</Show>
<Show when={!wide()}>
<text fg={theme.text}>
{keybind.print("sidebar_toggle")} <span style={{ fg: theme.textMuted }}>sidebar</span>
{keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span>
</text>
</Show>
<text fg={theme.text}>
{keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span>
</text>
</Match>
<Match when={store.mode === "shell"}>
<text fg={theme.text}>
esc <span style={{ fg: theme.textMuted }}>exit shell mode</span>
</text>
</Match>
</Switch>
</box>
</Match>
<Match when={store.mode === "shell"}>
<text fg={theme.text}>
esc <span style={{ fg: theme.textMuted }}>exit shell mode</span>
</text>
</Match>
</Switch>
</box>
</Show>
</box>
</box>
</>
@@ -1,140 +1,124 @@
import { type Accessor, createMemo, Match, Show, Switch } from "solid-js"
import { useRouteData } from "@tui/context/route"
import { useSync } from "@tui/context/sync"
import { pipe, sumBy } from "remeda"
import { useTheme } from "@tui/context/theme"
import { EmptyBorder } from "@tui/component/border"
import type { Session } from "@opencode-ai/sdk/v2"
import { SplitBorder, EmptyBorder } from "@tui/component/border"
import type { AssistantMessage, Session } from "@opencode-ai/sdk/v2"
import { useDirectory } from "../../context/directory"
import { useKeybind } from "../../context/keybind"
import { useTerminalDimensions } from "@opentui/solid"
const Title = (props: { session: Accessor<Session>; truncate?: boolean }) => {
const Title = (props: { session: Accessor<Session> }) => {
const { theme } = useTheme()
return (
<text fg={theme.text} wrapMode={props.truncate ? "none" : undefined} flexShrink={props.truncate ? 1 : 0}>
<text fg={theme.text}>
<span style={{ bold: true }}>#</span> <span style={{ bold: true }}>{props.session().title}</span>
</text>
)
}
const ContextInfo = (props: { context: Accessor<string | undefined>; cost: Accessor<string> }) => {
const { theme } = useTheme()
return (
<Show when={props.context()}>
<text fg={theme.textMuted} wrapMode="none" flexShrink={0}>
{props.context()} ({props.cost()})
</text>
</Show>
)
}
export function Header() {
const route = useRouteData("session")
const sync = useSync()
const session = createMemo(() => sync.session.get(route.sessionID)!)
const messages = createMemo(() => sync.data.message[route.sessionID] ?? [])
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
const showShare = createMemo(() => shareEnabled() && !session()?.share?.url)
const cost = createMemo(() => {
const total = pipe(
messages(),
sumBy((x) => (x.role === "assistant" ? x.cost : 0)),
)
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(total)
})
const context = createMemo(() => {
const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage
if (!last) return
const total =
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID]
let result = total.toLocaleString()
if (model?.limit.context) {
result += " " + Math.round((total / model.limit.context) * 100) + "%"
}
return result
})
const { theme } = useTheme()
const keybind = useKeybind()
const dimensions = useTerminalDimensions()
const tall = createMemo(() => dimensions().height > 40)
return (
<box flexShrink={0}>
<box
height={1}
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
paddingRight={1}
{...SplitBorder}
border={["left"]}
borderColor={theme.border}
customBorderChars={{
...EmptyBorder,
vertical: theme.backgroundPanel.a !== 0 ? "╻" : " ",
}}
flexShrink={0}
backgroundColor={theme.backgroundPanel}
>
<box
height={1}
border={["top"]}
borderColor={theme.backgroundPanel}
customBorderChars={
theme.backgroundPanel.a !== 0
? {
...EmptyBorder,
horizontal: "▄",
}
: {
...EmptyBorder,
horizontal: " ",
}
}
/>
</box>
<box
border={["left"]}
borderColor={theme.border}
customBorderChars={{
...EmptyBorder,
vertical: "┃",
bottomLeft: "╹",
}}
>
<box
paddingTop={tall() ? 1 : 0}
paddingBottom={tall() ? 1 : 0}
paddingLeft={2}
paddingRight={1}
flexShrink={0}
flexGrow={1}
backgroundColor={theme.backgroundPanel}
>
<Switch>
<Match when={session()?.parentID}>
<box flexDirection="row" gap={2}>
<text fg={theme.text}>
<b>Subagent session</b>
</text>
<text fg={theme.text}>
Parent <span style={{ fg: theme.textMuted }}>{keybind.print("session_parent")}</span>
</text>
<text fg={theme.text}>
Prev <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle_reverse")}</span>
</text>
<text fg={theme.text}>
Next <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle")}</span>
</text>
<box flexGrow={1} flexShrink={1} />
<Show when={showShare()}>
<text fg={theme.textMuted} wrapMode="none" flexShrink={0}>
/share{" "}
</text>
</Show>
</box>
</Match>
<Match when={true}>
<Switch>
<Match when={session()?.parentID}>
<box flexDirection="row" gap={2}>
<text fg={theme.text}>
<b>Subagent session</b>
</text>
<text fg={theme.text}>
Parent <span style={{ fg: theme.textMuted }}>{keybind.print("session_parent")}</span>
</text>
<text fg={theme.text}>
Prev <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle_reverse")}</span>
</text>
<text fg={theme.text}>
Next <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle")}</span>
</text>
<box flexGrow={1} flexShrink={1} />
<ContextInfo context={context} cost={cost} />
</box>
</Match>
<Match when={true}>
<box flexDirection="row" justifyContent="space-between" gap={1}>
<Title session={session} />
<ContextInfo context={context} cost={cost} />
</box>
<Show when={shareEnabled()}>
<box flexDirection="row" justifyContent="space-between" gap={1}>
<Title session={session} truncate={!tall()} />
<Show when={showShare()}>
<text fg={theme.textMuted} wrapMode="none" flexShrink={0}>
/share{" "}
</text>
</Show>
<box flexGrow={1} flexShrink={1}>
<Switch>
<Match when={session().share?.url}>
<text fg={theme.textMuted} wrapMode="word">
{session().share!.url}
</text>
</Match>
<Match when={true}>
<text fg={theme.text} wrapMode="word">
/share <span style={{ fg: theme.textMuted }}>copy link</span>
</text>
</Match>
</Switch>
</box>
</box>
</Match>
</Switch>
</box>
</box>
<box
height={1}
border={["left"]}
borderColor={theme.border}
customBorderChars={{
...EmptyBorder,
vertical: theme.backgroundPanel.a !== 0 ? "╹" : " ",
}}
>
<box
height={1}
border={["bottom"]}
borderColor={theme.backgroundPanel}
customBorderChars={
theme.backgroundPanel.a !== 0
? {
...EmptyBorder,
horizontal: "▀",
}
: {
...EmptyBorder,
horizontal: " ",
}
}
/>
</Show>
</Match>
</Switch>
</box>
</box>
)
@@ -22,7 +22,6 @@ import {
ScrollBoxRenderable,
addDefaultParsers,
MacOSScrollAccel,
RGBA,
type ScrollAcceleration,
} from "@opentui/core"
import { Prompt, type PromptRef } from "@tui/component/prompt"
@@ -131,15 +130,13 @@ export function Session() {
const [diffWrapMode, setDiffWrapMode] = createSignal<"word" | "none">("word")
const wide = createMemo(() => dimensions().width > 120)
const tall = createMemo(() => dimensions().height > 40)
const sidebarVisible = createMemo(() => {
if (session()?.parentID) return false
if (sidebar() === "show") return true
if (sidebar() === "auto" && wide()) return true
return false
})
const sidebarOverlay = createMemo(() => sidebarVisible() && !wide())
const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() && !sidebarOverlay() ? 42 : 0) - 4)
const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() ? 42 : 0) - 4)
const scrollAcceleration = createMemo(() => {
const tui = sync.data.config.tui
@@ -965,7 +962,7 @@ export function Session() {
<box flexDirection="row">
<box flexGrow={1} paddingBottom={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={1}>
<Show when={session()}>
<Show when={!sidebarVisible() || sidebarOverlay()}>
<Show when={!sidebarVisible()}>
<Header />
</Show>
<scrollbox
@@ -1095,33 +1092,15 @@ export function Session() {
sessionID={route.sessionID}
/>
</box>
<Show when={(!sidebarVisible() || sidebarOverlay()) && tall()}>
<Show when={!sidebarVisible()}>
<Footer />
</Show>
</Show>
<Toast />
</box>
<Show when={sidebarVisible() && !sidebarOverlay()}>
<Show when={sidebarVisible()}>
<Sidebar sessionID={route.sessionID} />
</Show>
<Show when={sidebarOverlay()}>
<box
position="absolute"
left={0}
top={0}
width={dimensions().width}
height={dimensions().height}
backgroundColor={RGBA.fromInts(0, 0, 0, 150)}
zIndex={100}
flexDirection="row"
justifyContent="flex-end"
onMouseUp={() => setSidebar("hide")}
>
<box onMouseUp={(e) => e.stopPropagation()}>
<Sidebar sessionID={route.sessionID} />
</box>
</box>
</Show>
</box>
</context.Provider>
)
@@ -73,7 +73,6 @@ export function Sidebar(props: { sessionID: string }) {
<box
backgroundColor={theme.backgroundPanel}
width={42}
height="100%"
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.0.195",
"version": "1.0.198",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.0.195",
"version": "1.0.198",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.0.195",
"version": "1.0.198",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.0.195",
"version": "1.0.198",
"type": "module",
"exports": {
"./*": "./src/components/*.tsx",
+5 -2
View File
@@ -415,8 +415,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
PART_MAPPING["text"] = function TextPartDisplay(props) {
const data = useData()
const part = props.part as TextPart
const content = createMemo(() => (part.text ?? "").trim())
const displayText = createMemo(() => relativizeProjectPaths(content(), data.directory))
const displayText = () => relativizeProjectPaths((part.text ?? "").trim(), data.directory)
return (
<Show when={displayText()}>
@@ -441,6 +440,9 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props) {
ToolRegistry.register({
name: "read",
render(props) {
const args: string[] = []
if (props.input.offset) args.push("offset=" + props.input.offset)
if (props.input.limit) args.push("limit=" + props.input.limit)
return (
<BasicTool
{...props}
@@ -448,6 +450,7 @@ ToolRegistry.register({
trigger={{
title: "Read",
subtitle: props.input.filePath ? getFilename(props.input.filePath) : "",
args,
}}
/>
)
+124 -98
View File
@@ -60,6 +60,31 @@ function computeStatusFromPart(part: PartType | undefined): string | undefined {
return undefined
}
function AssistantMessageItem(props: {
message: AssistantMessage
summary: string | undefined
response: string | undefined
lastTextPartId: string | undefined
working: boolean
}) {
const data = useData()
const msgParts = createMemo(() => data.store.part[props.message.id] ?? [])
const lastTextPart = createMemo(() =>
msgParts()
.filter((p) => p?.type === "text")
.at(-1),
)
const filteredParts = createMemo(() => {
if (!props.working && !props.summary && props.response && props.lastTextPartId === lastTextPart()?.id) {
return msgParts().filter((p) => p?.id !== lastTextPart()?.id)
}
return msgParts()
})
return <Message message={props.message} parts={filteredParts()} />
}
export function SessionTurn(
props: ParentProps<{
sessionID: string
@@ -77,66 +102,68 @@ export function SessionTurn(
const data = useData()
const diffComponent = useDiffComponent()
const derived = createMemo(() => {
const allMessages = data.store.message[props.sessionID] ?? []
const userMessages = allMessages.filter((m) => m.role === "user").sort((a, b) => a.id.localeCompare(b.id))
const lastUserMessage = userMessages.at(-1)
const message = userMessages.find((m) => m.id === props.messageID)
const allMessages = createMemo(() => data.store.message[props.sessionID] ?? [])
const userMessages = createMemo(() =>
allMessages()
.filter((m) => m.role === "user")
.sort((a, b) => a.id.localeCompare(b.id)),
)
if (!message) {
return {
message: undefined,
parts: [] as PartType[],
assistantMessages: [] as AssistantMessage[],
assistantParts: [] as PartType[],
lastAssistantMessage: undefined as AssistantMessage | undefined,
lastTextPart: undefined as PartType | undefined,
error: undefined,
hasSteps: false,
isShellMode: false,
rawStatus: undefined as string | undefined,
isLastUserMessage: false,
}
}
const message = createMemo(() => userMessages().find((m) => m.id === props.messageID))
const isLastUserMessage = createMemo(() => message()?.id === userMessages().at(-1)?.id)
const parts = data.store.part[message.id] ?? []
const assistantMessages = allMessages.filter(
(m) => m.role === "assistant" && m.parentID === message.id,
) as AssistantMessage[]
const parts = createMemo(() => {
const msg = message()
if (!msg) return []
return data.store.part[msg.id] ?? []
})
const assistantParts: PartType[] = []
for (const m of assistantMessages) {
const assistantMessages = createMemo(() => {
const msg = message()
if (!msg) return [] as AssistantMessage[]
return allMessages().filter((m) => m.role === "assistant" && m.parentID === msg.id) as AssistantMessage[]
})
const assistantParts = createMemo(() => {
const result: PartType[] = []
for (const m of assistantMessages()) {
const msgParts = data.store.part[m.id]
if (msgParts) {
for (const p of msgParts) {
if (p) assistantParts.push(p)
if (p) result.push(p)
}
}
}
return result
})
const lastAssistantMessage = assistantMessages.at(-1)
const error = assistantMessages.find((m) => m.error)?.error
const lastAssistantMessage = createMemo(() => assistantMessages().at(-1))
let lastTextPart: PartType | undefined
for (let i = assistantParts.length - 1; i >= 0; i--) {
if (assistantParts[i]?.type === "text") {
lastTextPart = assistantParts[i]
break
}
const error = createMemo(() => assistantMessages().find((m) => m.error)?.error)
const lastTextPart = createMemo(() => {
const ap = assistantParts()
for (let i = ap.length - 1; i >= 0; i--) {
if (ap[i]?.type === "text") return ap[i]
}
return undefined
})
const hasSteps = assistantParts.some((p) => p?.type === "tool")
const hasSteps = createMemo(() => assistantParts().some((p) => p?.type === "tool"))
let isShellMode = false
if (parts.every((p) => p?.type === "text" && p?.synthetic) && assistantParts.length === 1) {
const assistantPart = assistantParts[0]
if (assistantPart?.type === "tool" && assistantPart?.tool === "bash") {
isShellMode = true
}
const isShellMode = createMemo(() => {
const p = parts()
const ap = assistantParts()
if (p.every((part) => part?.type === "text" && part?.synthetic) && ap.length === 1) {
const assistantPart = ap[0]
if (assistantPart?.type === "tool" && assistantPart?.tool === "bash") return true
}
return false
})
let resolvedParts = assistantParts
const currentTask = assistantParts.findLast(
const rawStatus = createMemo(() => {
const ap = assistantParts()
const currentTask = ap.findLast(
(p) =>
p &&
p.type === "tool" &&
@@ -148,6 +175,7 @@ export function SessionTurn(
p.state.status === "running",
) as ToolPart | undefined
let resolvedParts = ap
if (currentTask?.state && "metadata" in currentTask.state && currentTask.state.metadata?.sessionId) {
const taskMessages = data.store.message[currentTask.state.metadata.sessionId as string]?.filter(
(m) => m.role === "assistant",
@@ -162,48 +190,20 @@ export function SessionTurn(
}
}
}
if (taskParts.length > 0) {
resolvedParts = taskParts
}
if (taskParts.length > 0) resolvedParts = taskParts
}
}
const lastPart = resolvedParts.at(-1)
const rawStatus = computeStatusFromPart(lastPart)
return {
message,
parts,
assistantMessages,
assistantParts,
lastAssistantMessage,
lastTextPart,
error,
hasSteps,
isShellMode,
rawStatus,
isLastUserMessage: message.id === lastUserMessage?.id,
}
return computeStatusFromPart(resolvedParts.at(-1))
})
const message = () => derived().message
const parts = () => derived().parts
const assistantMessages = () => derived().assistantMessages
const assistantParts = () => derived().assistantParts
const lastAssistantMessage = () => derived().lastAssistantMessage
const lastTextPart = () => derived().lastTextPart
const error = () => derived().error
const hasSteps = () => derived().hasSteps
const isShellMode = () => derived().isShellMode
const rawStatus = () => derived().rawStatus
const status = createMemo(
() =>
data.store.session_status[props.sessionID] ?? {
type: "idle",
},
)
const working = createMemo(() => status().type !== "idle" && derived().isLastUserMessage)
const working = createMemo(() => status().type !== "idle" && isLastUserMessage())
const retry = createMemo(() => {
const s = status()
if (s.type !== "retry") return
@@ -246,6 +246,7 @@ export function SessionTurn(
retrySeconds: 0,
status: rawStatus(),
duration: duration(),
summaryWaitTimedOut: false,
})
createEffect(() => {
@@ -286,6 +287,42 @@ export function SessionTurn(
onCleanup(() => clearInterval(timer))
})
createEffect(() => {
if (working()) {
setStore("summaryWaitTimedOut", false)
}
})
createEffect(() => {
if (working() || !isLastUserMessage()) return
const diffs = message()?.summary?.diffs
if (!diffs?.length) return
if (summary()) return
if (store.summaryWaitTimedOut) return
const timer = setTimeout(() => {
setStore("summaryWaitTimedOut", true)
}, 6000)
onCleanup(() => clearTimeout(timer))
})
const waitingForSummary = createMemo(() => {
if (!isLastUserMessage()) return false
if (working()) return false
const diffs = message()?.summary?.diffs
if (!diffs?.length) return false
if (summary()) return false
return !store.summaryWaitTimedOut
})
const showSummarySection = createMemo(() => {
if (working()) return false
return !waitingForSummary()
})
let lastStatusChange = Date.now()
let statusTimeout: number | undefined
createEffect(() => {
@@ -362,7 +399,7 @@ export function SessionTurn(
size="small"
onClick={props.onStepsExpandedToggle ?? (() => {})}
>
<Show when={working()}>
<Show when={working() || waitingForSummary()}>
<Spinner />
</Show>
<Switch>
@@ -379,6 +416,7 @@ export function SessionTurn(
</span>
<span data-slot="session-turn-retry-attempt">(#{retry()?.attempt})</span>
</Match>
<Match when={waitingForSummary()}>Generating summary</Match>
<Match when={working()}>{store.status ?? "Considering next steps"}</Match>
<Match when={props.stepsExpanded}>Hide steps</Match>
<Match when={!props.stepsExpanded}>Show steps</Match>
@@ -395,27 +433,15 @@ export function SessionTurn(
<Show when={props.stepsExpanded && assistantMessages().length > 0}>
<div data-slot="session-turn-collapsible-content-inner">
<For each={assistantMessages()}>
{(assistantMessage) => {
const parts = createMemo(() => data.store.part[assistantMessage.id] ?? [])
const last = createMemo(() =>
parts()
.filter((p) => p?.type === "text")
.at(-1),
)
return (
<Switch>
<Match when={!summary() && response() && lastTextPart()?.id === last()?.id}>
<Message
message={assistantMessage}
parts={parts().filter((p) => p?.id !== last()?.id)}
/>
</Match>
<Match when={true}>
<Message message={assistantMessage} parts={parts()} />
</Match>
</Switch>
)
}}
{(assistantMessage) => (
<AssistantMessageItem
message={assistantMessage}
summary={summary()}
response={response()}
lastTextPartId={lastTextPart()?.id}
working={working()}
/>
)}
</For>
<Show when={error()}>
<Card variant="error" class="error-card">
@@ -425,7 +451,7 @@ export function SessionTurn(
</div>
</Show>
{/* Summary */}
<Show when={!working()}>
<Show when={showSummarySection()}>
<div data-slot="session-turn-summary-section">
<div data-slot="session-turn-summary-header">
<Switch>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/util",
"version": "1.0.195",
"version": "1.0.198",
"private": true,
"type": "module",
"exports": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/web",
"type": "module",
"version": "1.0.195",
"version": "1.0.198",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
+224
View File
@@ -69,6 +69,16 @@ This command will guide you through creating a new agent with a custom system pr
---
#### list
List all available agents.
```bash
opencode agent list
```
---
### auth
Command to manage credentials and login for providers.
@@ -156,6 +166,88 @@ opencode github run
---
### mcp
Manage Model Context Protocol servers.
```bash
opencode mcp [command]
```
---
#### add
Add an MCP server to your configuration.
```bash
opencode mcp add
```
This command will guide you through adding either a local or remote MCP server.
---
#### list
List all configured MCP servers and their connection status.
```bash
opencode mcp list
```
Or use the short version.
```bash
opencode mcp ls
```
---
#### auth
Authenticate with an OAuth-enabled MCP server.
```bash
opencode mcp auth [name]
```
If you don't provide a server name, you'll be prompted to select from available OAuth-capable servers.
You can also list OAuth-capable servers and their authentication status.
```bash
opencode mcp auth list
```
Or use the short version.
```bash
opencode mcp auth ls
```
---
#### logout
Remove OAuth credentials for an MCP server.
```bash
opencode mcp logout [name]
```
---
#### debug
Debug OAuth connection issues for an MCP server.
```bash
opencode mcp debug <name>
```
---
### models
List all available models from configured providers.
@@ -250,6 +342,138 @@ This starts an HTTP server that provides API access to opencode functionality wi
---
### session
Manage OpenCode sessions.
```bash
opencode session [command]
```
---
#### list
List all OpenCode sessions.
```bash
opencode session list
```
##### Flags
| Flag | Short | Description |
| ------------- | ----- | ------------------------------------ |
| `--max-count` | `-n` | Limit to N most recent sessions |
| `--format` | | Output format: table or json (table) |
---
### stats
Show token usage and cost statistics for your OpenCode sessions.
```bash
opencode stats
```
#### Flags
| Flag | Description |
| ----------- | --------------------------------------------------------------- |
| `--days` | Show stats for the last N days (all time) |
| `--tools` | Number of tools to show (all) |
| `--project` | Filter by project (all projects, empty string: current project) |
---
### export
Export session data as JSON.
```bash
opencode export [sessionID]
```
If you don't provide a session ID, you'll be prompted to select from available sessions.
---
### import
Import session data from a JSON file or OpenCode share URL.
```bash
opencode import <file>
```
You can import from a local file or an OpenCode share URL.
```bash
opencode import session.json
opencode import https://opncd.ai/s/abc123
```
---
### web
Start a headless OpenCode server with a web interface.
```bash
opencode web
```
This starts an HTTP server and opens a web browser to access OpenCode through a web interface.
#### Flags
| Flag | Short | Description |
| ------------ | ----- | --------------------- |
| `--port` | `-p` | Port to listen on |
| `--hostname` | | Hostname to listen on |
---
### acp
Start an ACP (Agent Client Protocol) server.
```bash
opencode acp
```
This command starts an ACP server that communicates via stdin/stdout using nd-JSON.
#### Flags
| Flag | Description |
| ------------ | --------------------- |
| `--cwd` | Working directory |
| `--port` | Port to listen on |
| `--hostname` | Hostname to listen on |
---
### uninstall
Uninstall OpenCode and remove all related files.
```bash
opencode uninstall
```
#### Flags
| Flag | Short | Description |
| --------------- | ----- | ------------------------------------------- |
| `--keep-config` | `-c` | Keep configuration files |
| `--keep-data` | `-d` | Keep session data and snapshots |
| `--dry-run` | | Show what would be removed without removing |
| `--force` | `-f` | Skip confirmation prompts |
---
### upgrade
Updates opencode to the latest version or a specific version.
+16 -6
View File
@@ -28,11 +28,13 @@ OpenCode supports both **JSON** and **JSONC** (JSON with Comments) formats.
You can place your config in a couple of different locations and they have a
different order of precedence.
:::note[Config Merging]
Configuration files are **merged together**, not replaced. Settings from all config locations are combined using a deep merge strategy, where later configs override earlier ones only for conflicting keys. Non-conflicting settings from all configs are preserved.
:::note
Configuration files are **merged together**, not replaced.
:::
Configuration files are merged together, not replaced. Settings from the following config locations are combined. Where later configs override earlier ones only for conflicting keys. Non-conflicting settings from all configs are preserved.
For example, if your global config sets `theme: "opencode"` and `autoupdate: true`, and your project config sets `model: "anthropic/claude-sonnet-4-5"`, the final configuration will include all three settings.
:::
---
@@ -58,13 +60,15 @@ This is also safe to be checked into Git and uses the same schema as the global
### Custom path
You can also specify a custom config file path using the `OPENCODE_CONFIG` environment variable. Settings from this config are merged with and can override the global and project configs.
You can also specify a custom config file path using the `OPENCODE_CONFIG` environment variable.
```bash
export OPENCODE_CONFIG=/path/to/my/custom-config.json
opencode run "Hello world"
```
Settings from this config are merged with and **can override** the global and project configs.
---
### Custom directory
@@ -79,7 +83,7 @@ export OPENCODE_CONFIG_DIR=/path/to/my/config-directory
opencode run "Hello world"
```
Note: The custom directory is loaded after the global config and `.opencode` directories, so it can override their settings.
The custom directory is loaded after the global config and `.opencode` directories, so it **can override** their settings.
---
@@ -376,6 +380,10 @@ You can disable providers that are loaded automatically through the `disabled_pr
}
```
:::note
The `disabled_providers` takes priority over `enabled_providers`.
:::
The `disabled_providers` option accepts an array of provider IDs. When a provider is disabled:
- It won't be loaded even if environment variables are set.
@@ -398,9 +406,11 @@ You can specify an allowlist of providers through the `enabled_providers` option
This is useful when you want to restrict OpenCode to only use specific providers rather than disabling them one by one.
:::note
If a provider appears in both `enabled_providers` and `disabled_providers`, the `disabled_providers` takes priority for backwards compatibility.
The `disabled_providers` takes priority over `enabled_providers`.
:::
If a provider appears in both `enabled_providers` and `disabled_providers`, the `disabled_providers` takes priority for backwards compatibility.
---
## Variables
+100 -61
View File
@@ -3,37 +3,27 @@ title: MCP servers
description: Add local and remote MCP tools.
---
You can add external tools to OpenCode using the _Model Context Protocol_, or MCP.
OpenCode supports both:
- Local servers
- Remote servers
You can add external tools to OpenCode using the _Model Context Protocol_, or MCP. OpenCode supports both local and remote servers.
Once added, MCP tools are automatically available to the LLM alongside built-in tools.
---
## Caveats
#### Caveats
When you use an MCP server, it adds to the context. This can quickly add up if
you have a lot of tools. So we recommend being careful with which MCP servers
you use.
When you use an MCP server, it adds to the context. This can quickly add up if you have a lot of tools. So we recommend being careful with which MCP servers you use.
:::tip
MCP servers add to your context, so you want to be careful with which
ones you enable.
MCP servers add to your context, so you want to be careful with which ones you enable.
:::
Certain MCP servers, like the GitHub MCP server tend to add a lot of tokens and
can easily exceed the context limit.
Certain MCP servers, like the GitHub MCP server, tend to add a lot of tokens and can easily exceed the context limit.
---
## Configure
## Enable
You can define MCP servers in your OpenCode config under `mcp`. Add each MCP
with a unique name. You can refer to that MCP by name when prompting the LLM.
You can define MCP servers in your OpenCode config under `mcp`. Add each MCP with a unique name. You can refer to that MCP by name when prompting the LLM.
```jsonc title="opencode.jsonc" {6}
{
@@ -54,7 +44,7 @@ You can also disable a server by setting `enabled` to `false`. This is useful if
---
### Local
## Local
Add local MCP servers using `type` to `"local"` within the MCP object.
@@ -77,8 +67,7 @@ Add local MCP servers using `type` to `"local"` within the MCP object.
The command is how the local MCP server is started. You can also pass in a list of environment variables as well.
For example, here's how I can add the test
[`@modelcontextprotocol/server-everything`](https://www.npmjs.com/package/@modelcontextprotocol/server-everything) MCP server.
For example, here's how you can add the test [`@modelcontextprotocol/server-everything`](https://www.npmjs.com/package/@modelcontextprotocol/server-everything) MCP server.
```jsonc title="opencode.jsonc"
{
@@ -98,6 +87,8 @@ And to use it I can add `use the mcp_everything tool` to my prompts.
use the mcp_everything tool to add the number 3 and 4
```
---
#### Options
Here are all the options for configuring a local MCP server.
@@ -112,9 +103,9 @@ Here are all the options for configuring a local MCP server.
---
### Remote
## Remote
Add remote MCP servers under by setting `type` to `"remote"`.
Add remote MCP servers by setting `type` to `"remote"`.
```json title="opencode.json"
{
@@ -132,7 +123,9 @@ Add remote MCP servers under by setting `type` to `"remote"`.
}
```
Here the `url` is the URL of the remote MCP server and with the `headers` option you can pass in a list of headers.
The `url` is the URL of the remote MCP server and with the `headers` option you can pass in a list of headers.
---
#### Options
@@ -147,7 +140,7 @@ Here the `url` is the URL of the remote MCP server and with the `headers` option
---
### OAuth
## OAuth
OpenCode automatically handles OAuth authentication for remote MCP servers. When a server requires authentication, OpenCode will:
@@ -155,7 +148,9 @@ OpenCode automatically handles OAuth authentication for remote MCP servers. When
2. Use **Dynamic Client Registration (RFC 7591)** if supported by the server
3. Store tokens securely for future requests
#### Automatic OAuth
---
### Automatic
For most OAuth-enabled MCP servers, no special configuration is needed. Just configure the remote server:
@@ -173,11 +168,13 @@ For most OAuth-enabled MCP servers, no special configuration is needed. Just con
If the server requires authentication, OpenCode will prompt you to authenticate when you first try to use it. If not, you can [manually trigger the flow](#authenticating) with `opencode mcp auth <server-name>`.
#### Pre-registered Client
---
### Pre-registered
If you have client credentials from the MCP server provider, you can configure them:
```json title="opencode.json"
```json title="opencode.json" {7-11}
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
@@ -194,11 +191,39 @@ If you have client credentials from the MCP server provider, you can configure t
}
```
---
### Authenticating
You can manually trigger authentication or manage credentials.
Authenticate with a specific MCP server:
```bash
opencode mcp auth my-oauth-server
```
List all MCP servers and their auth status:
```bash
opencode mcp list
```
Remove stored credentials:
```bash
opencode mcp logout my-oauth-server
```
The `mcp auth` command will open your browser for authorization. After you authorize, OpenCode will store the tokens securely in `~/.local/share/opencode/mcp-auth.json`.
---
#### Disabling OAuth
If you want to disable automatic OAuth for a server (e.g., for servers that use API keys instead), set `oauth` to `false`:
```json title="opencode.json"
```json title="opencode.json" {7}
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
@@ -214,31 +239,16 @@ If you want to disable automatic OAuth for a server (e.g., for servers that use
}
```
---
#### OAuth Options
| Option | Type | Required | Description |
| -------------- | --------------- | -------- | -------------------------------------------------------------------------------- |
| `oauth` | Object \| false | | OAuth config object, or `false` to disable OAuth auto-detection. |
| `clientId` | String | | OAuth client ID. If not provided, dynamic client registration will be attempted. |
| `clientSecret` | String | | OAuth client secret, if required by the authorization server. |
| `scope` | String | | OAuth scopes to request during authorization. |
#### Authenticating
You can manually trigger authentication or manage credentials:
```bash
# Authenticate with a specific MCP server
opencode mcp auth my-oauth-server
# List all MCP servers and their auth status
opencode mcp list
# Remove stored credentials
opencode mcp logout my-oauth-server
```
The `mcp auth` command will open your browser for authorization. After you authorize, OpenCode will store the tokens securely in `~/.local/share/opencode/mcp-auth.json`.
| Option | Type | Description |
| -------------- | --------------- | -------------------------------------------------------------------------------- |
| `oauth` | Object \| false | OAuth config object, or `false` to disable OAuth auto-detection. |
| `clientId` | String | OAuth client ID. If not provided, dynamic client registration will be attempted. |
| `clientSecret` | String | OAuth client secret, if required by the authorization server. |
| `scope` | String | OAuth scopes to request during authorization. |
#### Debugging
@@ -258,8 +268,7 @@ The `mcp debug` command shows the current auth status, tests HTTP connectivity,
## Manage
Your MCPs are available as tools in OpenCode, alongside built-in tools. So you
can manage them through the OpenCode config like any other tool.
Your MCPs are available as tools in OpenCode, alongside built-in tools. So you can manage them through the OpenCode config like any other tool.
---
@@ -313,11 +322,10 @@ Here we are using the glob pattern `my-mcp*` to disable all MCPs.
### Per agent
If you have a large number of MCP servers you may want to only enable them per
agent and disable them globally. To do this:
If you have a large number of MCP servers you may want to only enable them per agent and disable them globally. To do this:
1. Disable it as a tool globally.
2. In your [agent config](/docs/agents#tools) enable the MCP server as a tool.
2. In your [agent config](/docs/agents#tools), enable the MCP server as a tool.
```json title="opencode.json" {11, 14-18}
{
@@ -360,6 +368,39 @@ Below are examples of some common MCP servers. You can submit a PR if you want t
---
### Sentry
Add the [Sentry MCP server](https://mcp.sentry.dev) to interact with your Sentry projects and issues.
```json title="opencode.json" {4-8}
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"sentry": {
"type": "remote",
"url": "https://mcp.sentry.dev/mcp",
"oauth": {}
}
}
}
```
After adding the configuration, authenticate with Sentry:
```bash
opencode mcp auth sentry
```
This will open a browser window to complete the OAuth flow and connect OpenCode to your Sentry account.
Once authenticated, you can use Sentry tools in your prompts to query issues, projects, and error data.
```txt "use sentry"
Show me the latest unresolved issues in my project. use sentry
```
---
### Context7
Add the [Context7 MCP server](https://github.com/upstash/context7) to search through docs.
@@ -401,8 +442,7 @@ Add `use context7` to your prompts to use Context7 MCP server.
Configure a Cloudflare Worker script to cache JSON API responses for five minutes. use context7
```
Alternatively, you can add something like this to your
[AGENTS.md](/docs/rules/).
Alternatively, you can add something like this to your [AGENTS.md](/docs/rules/).
```md title="AGENTS.md"
When you need to search docs, use `context7` tools.
@@ -432,9 +472,8 @@ Since we named our MCP server `gh_grep`, you can add `use the gh_grep tool` to y
What's the right way to set a custom domain in an SST Astro component? use the gh_grep tool
```
Alternatively, you can add something like this to your
[AGENTS.md](/docs/rules/).
Alternatively, you can add something like this to your [AGENTS.md](/docs/rules/).
```md title="AGENTS.md"
If you are unsure how to do something, use `gh_grep` to search code examples from github.
If you are unsure how to do something, use `gh_grep` to search code examples from GitHub.
```
+4 -2
View File
@@ -362,11 +362,13 @@ You can customize TUI behavior through your OpenCode config file.
---
## View customization
## Customization
You can customize various aspects of the TUI view using the command palette (`ctrl+x h` or `/help`). These settings persist across restarts.
### Username display
---
#### Username display
Toggle whether your username appears in chat messages. Access this through:
+1 -1
View File
@@ -47,7 +47,7 @@ if (!Script.preview) {
body: {
model: {
providerID: "opencode",
modelID: "gemini-3-flash",
modelID: "claude-sonnet-4-5",
},
parts: [
{
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.0.195",
"version": "1.0.198",
"publisher": "sst-dev",
"repository": {
"type": "git",