Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff3b68bd36 | ||
|
|
474b6fd3d1 | ||
|
|
6145b197f3 | ||
|
|
918eff9233 | ||
|
|
987e444828 | ||
|
|
99633cb299 | ||
|
|
f822331eb8 | ||
|
|
0f053769db | ||
|
|
ceeaf494c4 | ||
|
|
126d887e57 | ||
|
|
e5cfc24d6b | ||
|
|
7f8d659737 | ||
|
|
4b061653f2 | ||
|
|
eeed89f985 | ||
|
|
8ab533b616 | ||
|
|
09a399d8d6 | ||
|
|
b75575884a | ||
|
|
5688c9fd61 | ||
|
|
08a075df61 |
@@ -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
@@ -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:
|
||||
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.0.195",
|
||||
"version": "1.0.198",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.195",
|
||||
"version": "1.0.198",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
||||
@@ -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,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,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,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.0.195",
|
||||
"version": "1.0.198",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo -b",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.195",
|
||||
"version": "1.0.198",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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,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,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,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,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,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,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.195",
|
||||
"version": "1.0.198",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./*": "./src/components/*.tsx",
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.0.195",
|
||||
"version": "1.0.198",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
```
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ if (!Script.preview) {
|
||||
body: {
|
||||
model: {
|
||||
providerID: "opencode",
|
||||
modelID: "gemini-3-flash",
|
||||
modelID: "claude-sonnet-4-5",
|
||||
},
|
||||
parts: [
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user