Compare commits

..
22 Commits
Author SHA1 Message Date
opencode 585378cba0 release: v1.0.217
Update Nix Hashes / update (push) Has been cancelled
2025-12-30 19:01:36 +00:00
Dax Raad 8cd8393339 core: allow CORS requests from tauri://localhost 2025-12-30 13:58:41 -05:00
GitHub Action b184b2fb73 chore: generate 2025-12-30 18:34:33 +00:00
Aiden Cline c88c2da9be fix: move variant toggle to command bar 2025-12-30 12:33:50 -06:00
opencode 9b04081ae0 release: v1.0.216
Update Nix Hashes / update (push) Has been cancelled
2025-12-30 18:07:37 +00:00
Dax Raad 7d2d87fa2c core: allow CORS requests from *.opencode.ai subdomains 2025-12-30 13:04:18 -05:00
GitHub Action 787f37b382 chore: generate 2025-12-30 17:59:01 +00:00
ja 8fa1af851c style(nix): use idiomatic inherit syntax (#6457) 2025-12-30 11:58:28 -06:00
opencode 73bc3e704e release: v1.0.215
Update Nix Hashes / update (push) Has been cancelled
2025-12-30 17:09:08 +00:00
Adam 8d2feed30e fix(desktop): more defensive agent access 2025-12-30 11:03:34 -06:00
GitHub Action 2d8d4e5dee chore: generate 2025-12-30 16:52:42 +00:00
Fayçal Mitidji b3784588ae Fix: High CPU / memory leak when filtering model list window to empty results (#6435) 2025-12-30 10:52:06 -06:00
opencode 104d52bc38 release: v1.0.214
Update Nix Hashes / update (push) Has been cancelled
2025-12-30 16:37:22 +00:00
Adam dff1fe2d28 fix(desktop): sort servers by health 2025-12-30 10:34:55 -06:00
Adam 72ab4260ee fix(desktop): don't persist fallback server urls 2025-12-30 10:31:48 -06:00
Adam 9e9b4a0555 fix(share): broken share pages 2025-12-30 10:27:06 -06:00
Adam e53192889c fix(app): better text selection 2025-12-30 10:21:37 -06:00
Rohan Mukherjee 23bbfb3d15 fix: cloudflare provider information (#6426) 2025-12-30 09:45:09 -06:00
opencode-agent[bot]andopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> 37da005a01 docs: projects, find.files, notifications (#6438)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2025-12-30 09:44:09 -06:00
Didier Durand 8b708242f1 chore: fix various typos (#6429) 2025-12-30 09:42:04 -06:00
GitHub Action 339d2dcb98 chore: generate 2025-12-30 15:28:59 +00:00
Adam bbc8678164 fix(desktop): share projects across all local servers 2025-12-30 09:28:19 -06:00
47 changed files with 397 additions and 204 deletions
+15 -15
View File
@@ -22,7 +22,7 @@
}, },
"packages/app": { "packages/app": {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.0.213", "version": "1.0.217",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@@ -70,7 +70,7 @@
}, },
"packages/console/app": { "packages/console/app": {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.0.213", "version": "1.0.217",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "1.15.2", "@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
@@ -98,7 +98,7 @@
}, },
"packages/console/core": { "packages/console/core": {
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.0.213", "version": "1.0.217",
"dependencies": { "dependencies": {
"@aws-sdk/client-sts": "3.782.0", "@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1", "@jsx-email/render": "1.1.1",
@@ -125,7 +125,7 @@
}, },
"packages/console/function": { "packages/console/function": {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.0.213", "version": "1.0.217",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "2.0.0", "@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2", "@ai-sdk/openai": "2.0.2",
@@ -149,7 +149,7 @@
}, },
"packages/console/mail": { "packages/console/mail": {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.0.213", "version": "1.0.217",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
@@ -173,7 +173,7 @@
}, },
"packages/desktop": { "packages/desktop": {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"version": "1.0.213", "version": "1.0.217",
"dependencies": { "dependencies": {
"@opencode-ai/app": "workspace:*", "@opencode-ai/app": "workspace:*",
"@solid-primitives/storage": "catalog:", "@solid-primitives/storage": "catalog:",
@@ -201,7 +201,7 @@
}, },
"packages/enterprise": { "packages/enterprise": {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.0.213", "version": "1.0.217",
"dependencies": { "dependencies": {
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*", "@opencode-ai/util": "workspace:*",
@@ -230,7 +230,7 @@
}, },
"packages/function": { "packages/function": {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.0.213", "version": "1.0.217",
"dependencies": { "dependencies": {
"@octokit/auth-app": "8.0.1", "@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:", "@octokit/rest": "catalog:",
@@ -246,7 +246,7 @@
}, },
"packages/opencode": { "packages/opencode": {
"name": "opencode", "name": "opencode",
"version": "1.0.213", "version": "1.0.217",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -347,7 +347,7 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.0.213", "version": "1.0.217",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"zod": "catalog:", "zod": "catalog:",
@@ -367,7 +367,7 @@
}, },
"packages/sdk/js": { "packages/sdk/js": {
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.0.213", "version": "1.0.217",
"devDependencies": { "devDependencies": {
"@hey-api/openapi-ts": "0.88.1", "@hey-api/openapi-ts": "0.88.1",
"@tsconfig/node22": "catalog:", "@tsconfig/node22": "catalog:",
@@ -378,7 +378,7 @@
}, },
"packages/slack": { "packages/slack": {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.0.213", "version": "1.0.217",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1", "@slack/bolt": "^3.17.1",
@@ -391,7 +391,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.0.213", "version": "1.0.217",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@@ -429,7 +429,7 @@
}, },
"packages/util": { "packages/util": {
"name": "@opencode-ai/util", "name": "@opencode-ai/util",
"version": "1.0.213", "version": "1.0.217",
"dependencies": { "dependencies": {
"zod": "catalog:", "zod": "catalog:",
}, },
@@ -440,7 +440,7 @@
}, },
"packages/web": { "packages/web": {
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"version": "1.0.213", "version": "1.0.217",
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "12.6.3", "@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1", "@astrojs/markdown-remark": "6.3.1",
+3 -3
View File
@@ -17,7 +17,7 @@
"aarch64-darwin" "aarch64-darwin"
"x86_64-darwin" "x86_64-darwin"
]; ];
lib = nixpkgs.lib; inherit (nixpkgs) lib;
forEachSystem = lib.genAttrs systems; forEachSystem = lib.genAttrs systems;
pkgsFor = system: nixpkgs.legacyPackages.${system}; pkgsFor = system: nixpkgs.legacyPackages.${system};
packageJson = builtins.fromJSON (builtins.readFile ./packages/opencode/package.json); packageJson = builtins.fromJSON (builtins.readFile ./packages/opencode/package.json);
@@ -70,12 +70,12 @@
in in
{ {
default = mkPackage { default = mkPackage {
version = packageJson.version; inherit (packageJson) version;
src = ./.; src = ./.;
scripts = ./nix/scripts; scripts = ./nix/scripts;
target = bunTarget.${system}; target = bunTarget.${system};
modelsDev = "${modelsDev.${system}}/dist/_api.json"; modelsDev = "${modelsDev.${system}}/dist/_api.json";
mkNodeModules = mkNodeModules; inherit mkNodeModules;
}; };
} }
); );
+18 -10
View File
@@ -1,18 +1,26 @@
{ hash, lib, stdenvNoCC, bun, cacert, curl }: {
hash,
lib,
stdenvNoCC,
bun,
cacert,
curl,
}:
args: args:
stdenvNoCC.mkDerivation { stdenvNoCC.mkDerivation {
pname = "opencode-node_modules"; pname = "opencode-node_modules";
version = args.version; inherit (args) version src;
src = args.src;
impureEnvVars = impureEnvVars = lib.fetchers.proxyImpureEnvVars ++ [
lib.fetchers.proxyImpureEnvVars "GIT_PROXY_COMMAND"
++ [ "SOCKS_SERVER"
"GIT_PROXY_COMMAND" ];
"SOCKS_SERVER"
];
nativeBuildInputs = [ bun cacert curl ]; nativeBuildInputs = [
bun
cacert
curl
];
dontConfigure = true; dontConfigure = true;
+10 -7
View File
@@ -1,7 +1,13 @@
{ lib, stdenvNoCC, bun, ripgrep, makeBinaryWrapper }: {
lib,
stdenvNoCC,
bun,
ripgrep,
makeBinaryWrapper,
}:
args: args:
let let
scripts = args.scripts; inherit (args) scripts;
mkModules = mkModules =
attrs: attrs:
args.mkNodeModules ( args.mkNodeModules (
@@ -14,13 +20,10 @@ let
in in
stdenvNoCC.mkDerivation (finalAttrs: { stdenvNoCC.mkDerivation (finalAttrs: {
pname = "opencode"; pname = "opencode";
version = args.version; inherit (args) version src;
src = args.src;
node_modules = mkModules { node_modules = mkModules {
version = finalAttrs.version; inherit (finalAttrs) version src;
src = finalAttrs.src;
}; };
nativeBuildInputs = [ nativeBuildInputs = [
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.0.213", "version": "1.0.217",
"description": "", "description": "",
"type": "module", "type": "module",
"exports": { "exports": {
@@ -46,6 +46,25 @@ export function DialogSelectServer() {
const current = createMemo(() => items().find((x) => x === server.url) ?? items()[0]) const current = createMemo(() => items().find((x) => x === server.url) ?? items()[0])
const sortedItems = createMemo(() => {
const list = items()
if (!list.length) return list
const active = current()
const order = new Map(list.map((url, index) => [url, index] as const))
const rank = (value?: ServerStatus) => {
if (value?.healthy === true) return 0
if (value?.healthy === false) return 2
return 1
}
return list.slice().sort((a, b) => {
if (a === active) return -1
if (b === active) return 1
const diff = rank(store.status[a]) - rank(store.status[b])
if (diff !== 0) return diff
return (order.get(a) ?? 0) - (order.get(b) ?? 0)
})
})
async function refreshHealth() { async function refreshHealth() {
const results: Record<string, ServerStatus> = {} const results: Record<string, ServerStatus> = {}
await Promise.all( await Promise.all(
@@ -63,9 +82,14 @@ export function DialogSelectServer() {
onCleanup(() => clearInterval(interval)) onCleanup(() => clearInterval(interval))
}) })
function select(value: string) { function select(value: string, persist?: boolean) {
if (store.status[value]?.healthy === false) return if (!persist && store.status[value]?.healthy === false) return
dialog.close() dialog.close()
if (persist) {
server.add(value)
navigate("/")
return
}
server.setActive(value) server.setActive(value)
navigate("/") navigate("/")
} }
@@ -87,7 +111,7 @@ export function DialogSelectServer() {
} }
setStore("url", "") setStore("url", "")
select(value) select(value, true)
} }
return ( return (
@@ -96,7 +120,7 @@ export function DialogSelectServer() {
<List <List
search={{ placeholder: "Search servers", autofocus: true }} search={{ placeholder: "Search servers", autofocus: true }}
emptyMessage="No servers yet" emptyMessage="No servers yet"
items={items} items={sortedItems}
key={(x) => x} key={(x) => x}
current={current()} current={current()}
onSelect={(x) => { onSelect={(x) => {
+12 -5
View File
@@ -1115,11 +1115,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
setStore("imageAttachments", []) setStore("imageAttachments", [])
setStore("mode", "normal") setStore("mode", "normal")
const model = { const currentModel = local.model.current()
modelID: local.model.current()!.id, const currentAgent = local.agent.current()
providerID: local.model.current()!.provider.id, if (!currentModel || !currentAgent) {
console.warn("No agent or model available for prompt submission")
return
} }
const agent = local.agent.current()!.name const model = {
modelID: currentModel.id,
providerID: currentModel.provider.id,
}
const agent = currentAgent.name
if (isShellMode) { if (isShellMode) {
sdk.client.session sdk.client.session
@@ -1321,6 +1327,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
onInput={handleInput} onInput={handleInput}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
classList={{ classList={{
"select-text": true,
"w-full px-5 py-3 pr-12 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true, "w-full px-5 py-3 pr-12 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true,
"[&_[data-type=file]]:text-icon-info-active": true, "[&_[data-type=file]]:text-icon-info-active": true,
"font-mono!": store.mode === "shell", "font-mono!": store.mode === "shell",
@@ -1359,7 +1366,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
> >
<Select <Select
options={local.agent.list().map((agent) => agent.name)} options={local.agent.list().map((agent) => agent.name)}
current={local.agent.current().name} current={local.agent.current()?.name ?? ""}
onSelect={local.agent.set} onSelect={local.agent.set}
class="capitalize" class="capitalize"
variant="ghost" variant="ghost"
+1
View File
@@ -233,6 +233,7 @@ export const Terminal = (props: TerminalProps) => {
style={{ "background-color": terminalColors().background }} style={{ "background-color": terminalColors().background }}
classList={{ classList={{
...(local.classList ?? {}), ...(local.classList ?? {}),
"select-text": true,
"size-full px-6 py-3 font-mono": true, "size-full px-6 py-3 font-mono": true,
[local.class ?? ""]: !!local.class, [local.class ?? ""]: !!local.class,
}} }}
+30 -10
View File
@@ -65,23 +65,40 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const agent = (() => { const agent = (() => {
const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden)) const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
const [store, setStore] = createStore<{ const [store, setStore] = createStore<{
current: string current?: string
}>({ }>({
current: list()[0].name, current: list()[0]?.name,
}) })
return { return {
list, list,
current() { current() {
return list().find((x) => x.name === store.current)! const available = list()
if (available.length === 0) return undefined
return available.find((x) => x.name === store.current) ?? available[0]
}, },
set(name: string | undefined) { set(name: string | undefined) {
setStore("current", name ?? list()[0].name) const available = list()
if (available.length === 0) {
setStore("current", undefined)
return
}
if (name && available.some((x) => x.name === name)) {
setStore("current", name)
return
}
setStore("current", available[0].name)
}, },
move(direction: 1 | -1) { move(direction: 1 | -1) {
let next = list().findIndex((x) => x.name === store.current) + direction const available = list()
if (next < 0) next = list().length - 1 if (available.length === 0) {
if (next >= list().length) next = 0 setStore("current", undefined)
const value = list()[next] return
}
let next = available.findIndex((x) => x.name === store.current) + direction
if (next < 0) next = available.length - 1
if (next >= available.length) next = 0
const value = available[next]
if (!value) return
setStore("current", value.name) setStore("current", value.name)
if (value.model) if (value.model)
model.set({ model.set({
@@ -182,11 +199,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const current = createMemo(() => { const current = createMemo(() => {
const a = agent.current() const a = agent.current()
if (!a) return undefined
const key = getFirstValidModel( const key = getFirstValidModel(
() => ephemeral.model[a.name], () => ephemeral.model[a.name],
() => a.model, () => a.model,
fallbackModel, fallbackModel,
)! )
if (!key) return undefined
return find(key) return find(key)
}) })
@@ -232,7 +251,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
cycle, cycle,
set(model: ModelKey | undefined, options?: { recent?: boolean }) { set(model: ModelKey | undefined, options?: { recent?: boolean }) {
batch(() => { batch(() => {
setEphemeral("model", agent.current().name, model ?? fallbackModel()) const currentAgent = agent.current()
if (currentAgent) setEphemeral("model", currentAgent.name, model ?? fallbackModel())
if (model) updateVisibility(model, "show") if (model) updateVisibility(model, "show")
if (options?.recent && model) { if (options?.recent && model) {
const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID) const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID)
+45 -29
View File
@@ -23,6 +23,13 @@ export function serverDisplayName(url: string) {
.split("/")[0] .split("/")[0]
} }
function projectsKey(url: string) {
if (!url) return ""
const host = url.replace(/^https?:\/\//, "").split(":")[0]
if (host === "localhost" || host === "127.0.0.1") return "local"
return url
}
export const { use: useServer, provider: ServerProvider } = createSimpleContext({ export const { use: useServer, provider: ServerProvider } = createSimpleContext({
name: "Server", name: "Server",
init: (props: { defaultUrl: string }) => { init: (props: { defaultUrl: string }) => {
@@ -41,6 +48,19 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
function setActive(input: string) { function setActive(input: string) {
const url = normalizeServerUrl(input) const url = normalizeServerUrl(input)
if (!url) return if (!url) return
setActiveRaw(url)
}
function add(input: string) {
const url = normalizeServerUrl(input)
if (!url) return
const fallback = normalizeServerUrl(props.defaultUrl)
if (fallback && url === fallback) {
setActiveRaw(url)
return
}
batch(() => { batch(() => {
if (!store.list.includes(url)) { if (!store.list.includes(url)) {
setStore("list", store.list.length, url) setStore("list", store.list.length, url)
@@ -64,15 +84,10 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
createEffect(() => { createEffect(() => {
if (!ready()) return if (!ready()) return
if (active()) return
const url = normalizeServerUrl(props.defaultUrl) const url = normalizeServerUrl(props.defaultUrl)
if (!url) return if (!url) return
setActiveRaw(url)
batch(() => {
if (!store.list.includes(url)) {
setStore("list", store.list.length, url)
}
setActiveRaw(url)
})
}) })
const isReady = createMemo(() => ready() && !!active()) const isReady = createMemo(() => ready() && !!active())
@@ -100,7 +115,8 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
onCleanup(() => clearInterval(interval)) onCleanup(() => clearInterval(interval))
}) })
const projectsList = createMemo(() => store.projects[active()] ?? []) const origin = createMemo(() => projectsKey(active()))
const projectsList = createMemo(() => store.projects[origin()] ?? [])
return { return {
ready: isReady, ready: isReady,
@@ -115,51 +131,51 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
return store.list return store.list
}, },
setActive, setActive,
add: setActive, add,
remove, remove,
projects: { projects: {
list: projectsList, list: projectsList,
open(directory: string) { open(directory: string) {
const url = active() const key = origin()
if (!url) return if (!key) return
const current = store.projects[url] ?? [] const current = store.projects[key] ?? []
if (current.find((x) => x.worktree === directory)) return if (current.find((x) => x.worktree === directory)) return
setStore("projects", url, [{ worktree: directory, expanded: true }, ...current]) setStore("projects", key, [{ worktree: directory, expanded: true }, ...current])
}, },
close(directory: string) { close(directory: string) {
const url = active() const key = origin()
if (!url) return if (!key) return
const current = store.projects[url] ?? [] const current = store.projects[key] ?? []
setStore( setStore(
"projects", "projects",
url, key,
current.filter((x) => x.worktree !== directory), current.filter((x) => x.worktree !== directory),
) )
}, },
expand(directory: string) { expand(directory: string) {
const url = active() const key = origin()
if (!url) return if (!key) return
const current = store.projects[url] ?? [] const current = store.projects[key] ?? []
const index = current.findIndex((x) => x.worktree === directory) const index = current.findIndex((x) => x.worktree === directory)
if (index !== -1) setStore("projects", url, index, "expanded", true) if (index !== -1) setStore("projects", key, index, "expanded", true)
}, },
collapse(directory: string) { collapse(directory: string) {
const url = active() const key = origin()
if (!url) return if (!key) return
const current = store.projects[url] ?? [] const current = store.projects[key] ?? []
const index = current.findIndex((x) => x.worktree === directory) const index = current.findIndex((x) => x.worktree === directory)
if (index !== -1) setStore("projects", url, index, "expanded", false) if (index !== -1) setStore("projects", key, index, "expanded", false)
}, },
move(directory: string, toIndex: number) { move(directory: string, toIndex: number) {
const url = active() const key = origin()
if (!url) return if (!key) return
const current = store.projects[url] ?? [] const current = store.projects[key] ?? []
const fromIndex = current.findIndex((x) => x.worktree === directory) const fromIndex = current.findIndex((x) => x.worktree === directory)
if (fromIndex === -1 || fromIndex === toIndex) return if (fromIndex === -1 || fromIndex === toIndex) return
const result = [...current] const result = [...current]
const [item] = result.splice(fromIndex, 1) const [item] = result.splice(fromIndex, 1)
result.splice(toIndex, 0, item) result.splice(toIndex, 0, item)
setStore("projects", url, result) setStore("projects", key, result)
}, },
}, },
} }
+1 -1
View File
@@ -1015,7 +1015,7 @@ export default function Layout(props: ParentProps) {
} }
return ( return (
<div class="relative flex-1 min-h-0 flex flex-col"> <div class="relative flex-1 min-h-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
<Header <Header
navigateToProject={navigateToProject} navigateToProject={navigateToProject}
navigateToSession={navigateToSession} navigateToSession={navigateToSession}
+3 -3
View File
@@ -858,7 +858,7 @@ export default function Page() {
</Tabs.List> </Tabs.List>
</div> </div>
<Show when={diffs().length}> <Show when={diffs().length}>
<Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden contain-strict"> <Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden"> <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
<SessionReview <SessionReview
classes={{ classes={{
@@ -884,7 +884,7 @@ export default function Page() {
}, },
) )
return ( return (
<Tabs.Content value={tab} class="select-text mt-3"> <Tabs.Content value={tab} class="mt-3">
<Switch> <Switch>
<Match when={file()}> <Match when={file()}>
{(f) => ( {(f) => (
@@ -896,7 +896,7 @@ export default function Page() {
cacheKey: checksum(f().content?.content ?? ""), cacheKey: checksum(f().content?.content ?? ""),
}} }}
overflow="scroll" overflow="scroll"
class="pb-40" class="select-text pb-40"
/> />
)} )}
</Match> </Match>
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.0.213", "version": "1.0.217",
"type": "module", "type": "module",
"scripts": { "scripts": {
"typecheck": "tsgo --noEmit", "typecheck": "tsgo --noEmit",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.0.213", "version": "1.0.217",
"private": true, "private": true,
"type": "module", "type": "module",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.0.213", "version": "1.0.217",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.0.213", "version": "1.0.217",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"private": true, "private": true,
"version": "1.0.213", "version": "1.0.217",
"type": "module", "type": "module",
"scripts": { "scripts": {
"typecheck": "tsgo -b", "typecheck": "tsgo -b",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.0.213", "version": "1.0.217",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+8 -5
View File
@@ -3,6 +3,7 @@ import { FileRoutes } from "@solidjs/start/router"
import { Font } from "@opencode-ai/ui/font" import { Font } from "@opencode-ai/ui/font"
import { MetaProvider } from "@solidjs/meta" import { MetaProvider } from "@solidjs/meta"
import { MarkedProvider } from "@opencode-ai/ui/context/marked" import { MarkedProvider } from "@opencode-ai/ui/context/marked"
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
import { Suspense } from "solid-js" import { Suspense } from "solid-js"
import "./app.css" import "./app.css"
import { Favicon } from "@opencode-ai/ui/favicon" import { Favicon } from "@opencode-ai/ui/favicon"
@@ -12,11 +13,13 @@ export default function App() {
<Router <Router
root={(props) => ( root={(props) => (
<MetaProvider> <MetaProvider>
<MarkedProvider> <DialogProvider>
<Favicon /> <MarkedProvider>
<Font /> <Favicon />
<Suspense>{props.children}</Suspense> <Font />
</MarkedProvider> <Suspense>{props.children}</Suspense>
</MarkedProvider>
</DialogProvider>
</MetaProvider> </MetaProvider>
)} )}
> >
@@ -162,11 +162,20 @@ export default function () {
return ( return (
<ErrorBoundary <ErrorBoundary
fallback={(e) => { fallback={(error) => {
if (SessionDataMissingError.isInstance(error)) {
return <NotFound />
}
console.error(error)
const details = error instanceof Error ? (error.stack ?? error.message) : String(error)
return ( return (
<Show when={e.message === "SessionDataMissingError"}> <div class="min-h-screen w-full bg-background-base text-text-base flex flex-col items-center justify-center gap-4 p-6 text-center">
<NotFound /> <p class="text-16-medium">Unable to render this share.</p>
</Show> <p class="text-14-regular text-text-weaker">Check the console for more details.</p>
<pre class="text-12-mono text-left whitespace-pre-wrap break-words w-full max-w-200 bg-background-stronger rounded-md p-4">
{details}
</pre>
</div>
) )
}} }}
> >
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode" id = "opencode"
name = "OpenCode" name = "OpenCode"
description = "The open source coding agent." description = "The open source coding agent."
version = "1.0.213" version = "1.0.217"
schema_version = 1 schema_version = 1
authors = ["Anomaly"] authors = ["Anomaly"]
repository = "https://github.com/sst/opencode" repository = "https://github.com/sst/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg" icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64] [agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.213/opencode-darwin-arm64.zip" archive = "https://github.com/sst/opencode/releases/download/v1.0.217/opencode-darwin-arm64.zip"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64] [agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.213/opencode-darwin-x64.zip" archive = "https://github.com/sst/opencode/releases/download/v1.0.217/opencode-darwin-x64.zip"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64] [agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.213/opencode-linux-arm64.tar.gz" archive = "https://github.com/sst/opencode/releases/download/v1.0.217/opencode-linux-arm64.tar.gz"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64] [agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.213/opencode-linux-x64.tar.gz" archive = "https://github.com/sst/opencode/releases/download/v1.0.217/opencode-linux-x64.tar.gz"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64] [agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.213/opencode-windows-x64.zip" archive = "https://github.com/sst/opencode/releases/download/v1.0.217/opencode-windows-x64.zip"
cmd = "./opencode.exe" cmd = "./opencode.exe"
args = ["acp"] args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.0.213", "version": "1.0.217",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.0.213", "version": "1.0.217",
"name": "opencode", "name": "opencode",
"type": "module", "type": "module",
"private": true, "private": true,
+6
View File
@@ -349,6 +349,12 @@ export const AuthLoginCommand = cmd({
prompts.log.info("You can create an api key at https://vercel.link/ai-gateway-token") prompts.log.info("You can create an api key at https://vercel.link/ai-gateway-token")
} }
if (["cloudflare", "cloudflare-ai-gateway"].includes(provider)) {
prompts.log.info(
"Cloudflare AI Gateway can be configured with CLOUDFLARE_GATEWAY_ID, CLOUDFLARE_ACCOUNT_ID, and CLOUDFLARE_API_TOKEN environment variables. Read more: https://opencode.ai/docs/providers/#cloudflare-ai-gateway",
)
}
const key = await prompts.password({ const key = await prompts.password({
message: "Enter your API key", message: "Enter your API key",
validate: (x) => (x && x.length > 0 ? undefined : "Required"), validate: (x) => (x && x.length > 0 ? undefined : "Required"),
@@ -372,6 +372,15 @@ function App() {
local.agent.move(1) local.agent.move(1)
}, },
}, },
{
title: "Variant cycle",
value: "variant.cycle",
keybind: "variant_cycle",
category: "Agent",
onSelect: () => {
local.model.variant.cycle()
},
},
{ {
title: "Agent cycle reverse", title: "Agent cycle reverse",
value: "agent.cycle.reverse", value: "agent.cycle.reverse",
@@ -877,12 +877,6 @@ export function Prompt(props: PromptProps) {
return return
} }
} }
if (keybind.match("variant_cycle", e)) {
e.preventDefault()
if (local.model.variant.list().length === 0) return
local.model.variant.cycle()
return
}
if (store.mode === "normal") autocomplete.onKeyDown(e) if (store.mode === "normal") autocomplete.onKeyDown(e)
if (!autocomplete.visible) { if (!autocomplete.visible) {
if ( if (
@@ -115,11 +115,12 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
setStore("selected", currentIndex) setStore("selected", currentIndex)
} }
} }
scroll.scrollTo(0) scroll?.scrollTo(0)
}), }),
) )
function move(direction: number) { function move(direction: number) {
if (flat().length === 0) return
let next = store.selected + direction let next = store.selected + direction
if (next < 0) next = flat().length - 1 if (next < 0) next = flat().length - 1
if (next >= flat().length) next = 0 if (next >= flat().length) next = 0
@@ -129,6 +130,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
function moveTo(next: number) { function moveTo(next: number) {
setStore("selected", next) setStore("selected", next)
props.onMove?.(selected()!) props.onMove?.(selected()!)
if (!scroll) return
const target = scroll.getChildren().find((child) => { const target = scroll.getChildren().find((child) => {
return child.id === JSON.stringify(selected()?.value) return child.id === JSON.stringify(selected()?.value)
}) })
@@ -172,7 +174,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
} }
}) })
let scroll: ScrollBoxRenderable let scroll: ScrollBoxRenderable | undefined
const ref: DialogSelectRef<T> = { const ref: DialogSelectRef<T> = {
get filter() { get filter() {
return store.filter return store.filter
@@ -213,61 +215,70 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
/> />
</box> </box>
</box> </box>
<scrollbox <Show
paddingLeft={1} when={grouped().length > 0}
paddingRight={1} fallback={
scrollbarOptions={{ visible: false }} <box paddingLeft={4} paddingRight={4} paddingTop={1}>
ref={(r: ScrollBoxRenderable) => (scroll = r)} <text fg={theme.textMuted}>No results found</text>
maxHeight={height()} </box>
}
> >
<For each={grouped()}> <scrollbox
{([category, options], index) => ( paddingLeft={1}
<> paddingRight={1}
<Show when={category}> scrollbarOptions={{ visible: false }}
<box paddingTop={index() > 0 ? 1 : 0} paddingLeft={3}> ref={(r: ScrollBoxRenderable) => (scroll = r)}
<text fg={theme.accent} attributes={TextAttributes.BOLD}> maxHeight={height()}
{category} >
</text> <For each={grouped()}>
</box> {([category, options], index) => (
</Show> <>
<For each={options}> <Show when={category}>
{(option) => { <box paddingTop={index() > 0 ? 1 : 0} paddingLeft={3}>
const active = createMemo(() => isDeepEqual(option.value, selected()?.value)) <text fg={theme.accent} attributes={TextAttributes.BOLD}>
const current = createMemo(() => isDeepEqual(option.value, props.current)) {category}
return ( </text>
<box </box>
id={JSON.stringify(option.value)} </Show>
flexDirection="row" <For each={options}>
onMouseUp={() => { {(option) => {
option.onSelect?.(dialog) const active = createMemo(() => isDeepEqual(option.value, selected()?.value))
props.onSelect?.(option) const current = createMemo(() => isDeepEqual(option.value, props.current))
}} return (
onMouseOver={() => { <box
const index = filtered().findIndex((x) => isDeepEqual(x.value, option.value)) id={JSON.stringify(option.value)}
if (index === -1) return flexDirection="row"
moveTo(index) onMouseUp={() => {
}} option.onSelect?.(dialog)
backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)} props.onSelect?.(option)
paddingLeft={current() || option.gutter ? 1 : 3} }}
paddingRight={3} onMouseOver={() => {
gap={1} const index = filtered().findIndex((x) => isDeepEqual(x.value, option.value))
> if (index === -1) return
<Option moveTo(index)
title={option.title} }}
footer={option.footer} backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)}
description={option.description !== category ? option.description : undefined} paddingLeft={current() || option.gutter ? 1 : 3}
active={active()} paddingRight={3}
current={current()} gap={1}
gutter={option.gutter} >
/> <Option
</box> title={option.title}
) footer={option.footer}
}} description={option.description !== category ? option.description : undefined}
</For> active={active()}
</> current={current()}
)} gutter={option.gutter}
</For> />
</scrollbox> </box>
)
}}
</For>
</>
)}
</For>
</scrollbox>
</Show>
<Show when={keybinds().length} fallback={<box flexShrink={0} />}> <Show when={keybinds().length} fallback={<box flexShrink={0} />}>
<box paddingRight={2} paddingLeft={4} flexDirection="row" gap={2} flexShrink={0} paddingTop={1}> <box paddingRight={2} paddingLeft={4} flexDirection="row" gap={2} flexShrink={0} paddingTop={1}>
<For each={keybinds()}> <For each={keybinds()}>
+17 -1
View File
@@ -104,7 +104,23 @@ export namespace Server {
timer.stop() timer.stop()
} }
}) })
.use(cors()) .use(
cors({
origin(input) {
if (!input) return
if (input.startsWith("http://localhost:")) return input
if (input.startsWith("http://127.0.0.1:")) return input
if (input === "tauri://localhost") return input
// *.opencode.ai (https only, adjust if needed)
if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) {
return input
}
return
},
}),
)
.get( .get(
"/global/health", "/global/health",
describeRoute({ describeRoute({
@@ -240,7 +240,7 @@ You are producing plain text that will later be styled by the CLI. Follow these
- Choose descriptive names that fit the content - Choose descriptive names that fit the content
- Keep headers short (13 words) and in `**Title Case**`. Always start headers with `**` and end with `**` - Keep headers short (13 words) and in `**Title Case**`. Always start headers with `**` and end with `**`
- Leave no blank line before the first bullet under a header. - Leave no blank line before the first bullet under a header.
- Section headers should only be used where they genuinely improve scanability; avoid fragmenting the answer. - Section headers should only be used where they genuinely improve scannability; avoid fragmenting the answer.
**Bullets** **Bullets**
@@ -289,7 +289,7 @@ When referencing files in your response, make sure to include the relevant start
- Dont nest bullets or create deep hierarchies. - Dont nest bullets or create deep hierarchies.
- Dont output ANSI escape codes directly — the CLI renderer applies them. - Dont output ANSI escape codes directly — the CLI renderer applies them.
- Dont cram unrelated keywords into a single bullet; split for clarity. - Dont cram unrelated keywords into a single bullet; split for clarity.
- Dont let keyword lists run long — wrap or reformat for scanability. - Dont let keyword lists run long — wrap or reformat for scannability.
Generally, ensure your final answers adapt their shape and depth to the request. For example, answers to code explanations should have a precise, structured explanation with code references that answer the question directly. For tasks with a simple implementation, lead with the outcome and supplement only with whats needed for clarity. Larger changes can be presented as a logical walkthrough of your approach, grouping related steps, explaining rationale where it adds value, and highlighting next actions to accelerate the user. Your answers should provide the right level of detail while being easily scannable. Generally, ensure your final answers adapt their shape and depth to the request. For example, answers to code explanations should have a precise, structured explanation with code references that answer the question directly. For tasks with a simple implementation, lead with the outcome and supplement only with whats needed for clarity. Larger changes can be presented as a logical walkthrough of your approach, grouping related steps, explaining rationale where it adds value, and highlighting next actions to accelerate the user. Your answers should provide the right level of detail while being easily scannable.
@@ -12,7 +12,7 @@ is a critical violation. ZERO exceptions.
## Responsibility ## Responsibility
Your current responsibility is to think, read, search, and delegate explore agents to construct a well formed plan that accomplishes the goal the user wants to achieve. Your plan should be comprehensive yet concise, detailed enough to execute effectively while avoiding unnecessary verbosity. Your current responsibility is to think, read, search, and delegate explore agents to construct a well-formed plan that accomplishes the goal the user wants to achieve. Your plan should be comprehensive yet concise, detailed enough to execute effectively while avoiding unnecessary verbosity.
Ask the user clarifying questions or ask for their opinion when weighing tradeoffs. Ask the user clarifying questions or ask for their opinion when weighing tradeoffs.
@@ -22,5 +22,5 @@ Ask the user clarifying questions or ask for their opinion when weighing tradeof
## Important ## Important
The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received.
</system-reminder> </system-reminder>
+1 -1
View File
@@ -280,7 +280,7 @@ export const BashTool = Tool.define("bash", async () => {
} }
if (timedOut) { if (timedOut) {
resultMetadata.push(`bash tool terminated commmand after exceeding timeout ${timeout} ms`) resultMetadata.push(`bash tool terminated command after exceeding timeout ${timeout} ms`)
} }
if (aborted) { if (aborted) {
+1 -1
View File
@@ -1,4 +1,4 @@
Launch a new agent to handle complex, multi-step tasks autonomously. Launch a new agent to handle complex, multistep tasks autonomously.
Available agent types and the tools they have access to: Available agent types and the tools they have access to:
{agents} {agents}
+1 -1
View File
@@ -4,7 +4,7 @@ It also helps the user understand the progress of the task and overall progress
## When to Use This Tool ## When to Use This Tool
Use this tool proactively in these scenarios: Use this tool proactively in these scenarios:
1. Complex multi-step tasks - When a task requires 3 or more distinct steps or actions 1. Complex multistep tasks - When a task requires 3 or more distinct steps or actions
2. Non-trivial and complex tasks - Tasks that require careful planning or multiple operations 2. Non-trivial and complex tasks - Tasks that require careful planning or multiple operations
3. User explicitly requests todo list - When the user directly asks you to use the todo list 3. User explicitly requests todo list - When the user directly asks you to use the todo list
4. User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated) 4. User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated)
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.0.213", "version": "1.0.217",
"type": "module", "type": "module",
"scripts": { "scripts": {
"typecheck": "tsgo --noEmit", "typecheck": "tsgo --noEmit",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.0.213", "version": "1.0.217",
"type": "module", "type": "module",
"scripts": { "scripts": {
"typecheck": "tsgo --noEmit", "typecheck": "tsgo --noEmit",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.0.213", "version": "1.0.217",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "bun run src/index.ts", "dev": "bun run src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.0.213", "version": "1.0.217",
"type": "module", "type": "module",
"exports": { "exports": {
"./*": "./src/components/*.tsx", "./*": "./src/components/*.tsx",
@@ -377,6 +377,18 @@
} }
} }
[data-component="user-message"] [data-slot="user-message-text"],
[data-component="text-part"],
[data-component="reasoning-part"],
[data-component="tool-error"],
[data-component="tool-output"],
[data-component="edit-content"],
[data-component="write-content"],
[data-component="todos"],
[data-component="diagnostics"] {
user-select: text;
}
[data-component="tool-part-wrapper"] { [data-component="tool-part-wrapper"] {
width: 100%; width: 100%;
@@ -65,6 +65,10 @@
} }
} }
[data-slot="accordion-content"] {
user-select: text;
}
[data-slot="session-review-trigger-content"] { [data-slot="session-review-trigger-content"] {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -116,6 +116,11 @@
color: var(--text-weak); color: var(--text-weak);
} }
[data-slot="session-turn-markdown"],
[data-slot="session-turn-accordion"] [data-slot="accordion-content"] {
user-select: text;
}
[data-slot="session-turn-markdown"] { [data-slot="session-turn-markdown"] {
&[data-diffs="true"] { &[data-diffs="true"] {
font-size: 15px; font-size: 15px;
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/util", "name": "@opencode-ai/util",
"version": "1.0.213", "version": "1.0.217",
"private": true, "private": true,
"type": "module", "type": "module",
"exports": { "exports": {
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"type": "module", "type": "module",
"version": "1.0.213", "version": "1.0.217",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
@@ -118,6 +118,29 @@ You can disable a keybind by adding the key to your config with a value of "none
--- ---
## Desktop prompt shortcuts
The OpenCode desktop app prompt input supports common Readline/Emacs-style shortcuts for editing text. These are built-in and currently not configurable via `opencode.json`.
| Shortcut | Action |
| -------- | ---------------------------------------- |
| `ctrl+a` | Move to start of current line |
| `ctrl+e` | Move to end of current line |
| `ctrl+b` | Move cursor back one character |
| `ctrl+f` | Move cursor forward one character |
| `alt+b` | Move cursor back one word |
| `alt+f` | Move cursor forward one word |
| `ctrl+d` | Delete character under cursor |
| `ctrl+k` | Kill to end of line |
| `ctrl+u` | Kill to start of line |
| `ctrl+w` | Kill previous word |
| `alt+d` | Kill next word |
| `ctrl+y` | Yank (paste) last killed text |
| `ctrl+t` | Transpose characters |
| `ctrl+g` | Cancel popovers / abort running response |
---
## Shift+Enter ## Shift+Enter
Some terminals don't send modifier keys with Enter by default. You may need to configure your terminal to send `Shift+Enter` as an escape sequence. Some terminals don't send modifier keys with Enter by default. You may need to configure your terminal to send `Shift+Enter` as an escape sequence.
@@ -230,6 +230,10 @@ export const NotificationPlugin = async ({ project, client, $, directory, worktr
We are using `osascript` to run AppleScript on macOS. Here we are using it to send notifications. We are using `osascript` to run AppleScript on macOS. Here we are using it to send notifications.
:::note
If youre using the OpenCode desktop app, it can send system notifications automatically when a response is ready or when a session errors.
:::
--- ---
### .env protection ### .env protection
+18 -8
View File
@@ -283,13 +283,19 @@ await client.session.prompt({
### Files ### Files
| Method | Description | Response | | Method | Description | Response |
| ------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------- | | ------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------- |
| `find.text({ query })` | Search for text in files | Array of match objects with `path`, `lines`, `line_number`, `absolute_offset`, `submatches` | | `find.text({ query })` | Search for text in files | Array of match objects with `path`, `lines`, `line_number`, `absolute_offset`, `submatches` |
| `find.files({ query })` | Find files by name | `string[]` (file paths) | | `find.files({ query })` | Find files and directories by name | `string[]` (paths) |
| `find.symbols({ query })` | Find workspace symbols | <a href={typesUrl}><code>Symbol[]</code></a> | | `find.symbols({ query })` | Find workspace symbols | <a href={typesUrl}><code>Symbol[]</code></a> |
| `file.read({ query })` | Read a file | `{ type: "raw" \| "patch", content: string }` | | `file.read({ query })` | Read a file | `{ type: "raw" \| "patch", content: string }` |
| `file.status({ query? })` | Get status for tracked files | <a href={typesUrl}><code>File[]</code></a> | | `file.status({ query? })` | Get status for tracked files | <a href={typesUrl}><code>File[]</code></a> |
`find.files` supports a few optional query fields:
- `type`: `"file"` or `"directory"`
- `directory`: override the project root for the search
- `limit`: max results (1200)
--- ---
@@ -302,7 +308,11 @@ const textResults = await client.find.text({
}) })
const files = await client.find.files({ const files = await client.find.files({
query: { query: "*.ts" }, query: { query: "*.ts", type: "file" },
})
const directories = await client.find.files({
query: { query: "packages", type: "directory", limit: 20 },
}) })
const content = await client.file.read({ const content = await client.file.read({
+16 -8
View File
@@ -173,14 +173,22 @@ The opencode server exposes the following APIs.
### Files ### Files
| Method | Path | Description | Response | | Method | Path | Description | Response |
| ------ | ------------------------ | ---------------------------- | ------------------------------------------------------------------------------------------- | | ------ | ------------------------ | ---------------------------------- | ------------------------------------------------------------------------------------------- |
| `GET` | `/find?pattern=<pat>` | Search for text in files | Array of match objects with `path`, `lines`, `line_number`, `absolute_offset`, `submatches` | | `GET` | `/find?pattern=<pat>` | Search for text in files | Array of match objects with `path`, `lines`, `line_number`, `absolute_offset`, `submatches` |
| `GET` | `/find/file?query=<q>` | Find files by name | `string[]` (file paths) | | `GET` | `/find/file?query=<q>` | Find files and directories by name | `string[]` (paths) |
| `GET` | `/find/symbol?query=<q>` | Find workspace symbols | <a href={typesUrl}><code>Symbol[]</code></a> | | `GET` | `/find/symbol?query=<q>` | Find workspace symbols | <a href={typesUrl}><code>Symbol[]</code></a> |
| `GET` | `/file?path=<path>` | List files and directories | <a href={typesUrl}><code>FileNode[]</code></a> | | `GET` | `/file?path=<path>` | List files and directories | <a href={typesUrl}><code>FileNode[]</code></a> |
| `GET` | `/file/content?path=<p>` | Read a file | <a href={typesUrl}><code>FileContent</code></a> | | `GET` | `/file/content?path=<p>` | Read a file | <a href={typesUrl}><code>FileContent</code></a> |
| `GET` | `/file/status` | Get status for tracked files | <a href={typesUrl}><code>File[]</code></a> | | `GET` | `/file/status` | Get status for tracked files | <a href={typesUrl}><code>File[]</code></a> |
#### `/find/file` query parameters
- `query` (required) — search string (fuzzy match)
- `type` (optional) — limit results to `"file"` or `"directory"`
- `directory` (optional) — override the project root for the search
- `limit` (optional) — max results (1200)
- `dirs` (optional) — legacy flag (`"false"` returns only files)
--- ---
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode", "name": "opencode",
"displayName": "opencode", "displayName": "opencode",
"description": "opencode for VS Code", "description": "opencode for VS Code",
"version": "1.0.213", "version": "1.0.217",
"publisher": "sst-dev", "publisher": "sst-dev",
"repository": { "repository": {
"type": "git", "type": "git",