Compare commits

...

8 Commits

Author SHA1 Message Date
Aiden Cline a0b17439f9 core: improve plugin loading to handle builtin plugin failures gracefully 2026-01-03 00:51:13 -06:00
GitHub Action 1261b7d333 chore: generate 2026-01-02 22:58:02 +00:00
YeonGyu-Kim a3f38e0533 feat(plugin): add tui.session.select API endpoint for TUI navigation (#6565)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-01-02 16:57:21 -06:00
GitHub Action 681a257df6 chore: generate 2026-01-02 22:46:22 +00:00
Troy Gaines 586207adb4 feat: Add kotlin lsp integration (#6601) 2026-01-02 16:45:44 -06:00
theavgjojo a58dbb3b5c chore: add license field to package.json (#6693)
publish-github-action / publish (push) Has been cancelled
Update Nix Hashes / update (push) Has been cancelled
Co-authored-by: theavgjojo <jojo@noreply>
2026-01-02 16:29:09 -06:00
Spoon 131d8e5778 docs: add subtask2 to ecosystem page (#6704) 2026-01-02 16:26:06 -06:00
Dax Raad 0cf0294787 anomalyco/opencode
publish-github-action / publish (push) Has been cancelled
Update Nix Hashes / update (push) Has been cancelled
2026-01-02 16:09:06 -05:00
30 changed files with 407 additions and 5 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
- uses: ./.github/actions/setup-bun
- name: Run opencode
uses: sst/opencode/github@latest
uses: anomalyco/opencode/github@latest
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
OPENCODE_PERMISSION: '{"bash": "deny"}'
+1
View File
@@ -3,6 +3,7 @@
"module": "index.ts",
"type": "module",
"private": true,
"license": "MIT",
"devDependencies": {
"@types/bun": "catalog:"
},
+1
View File
@@ -2,6 +2,7 @@
"name": "@opencode-ai/console-app",
"version": "1.0.224",
"type": "module",
"license": "MIT",
"scripts": {
"typecheck": "tsgo --noEmit",
"dev": "vite dev --host 0.0.0.0",
+1
View File
@@ -4,6 +4,7 @@
"version": "1.0.224",
"private": true,
"type": "module",
"license": "MIT",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
+1
View File
@@ -4,6 +4,7 @@
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
"license": "MIT",
"scripts": {
"typecheck": "tsgo --noEmit"
},
+2 -1
View File
@@ -17,5 +17,6 @@
"scripts": {
"dev": "email preview emails/templates"
},
"type": "module"
"type": "module",
"license": "MIT"
}
+1
View File
@@ -1,6 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-resource",
"license": "MIT",
"dependencies": {
"@cloudflare/workers-types": "catalog:"
},
+1
View File
@@ -3,6 +3,7 @@
"private": true,
"version": "1.0.224",
"type": "module",
"license": "MIT",
"scripts": {
"typecheck": "tsgo -b",
"predev": "bun ./scripts/predev.ts",
+1
View File
@@ -3,6 +3,7 @@
"version": "1.0.224",
"private": true,
"type": "module",
"license": "MIT",
"scripts": {
"typecheck": "tsgo --noEmit",
"dev": "vite dev",
+1
View File
@@ -4,6 +4,7 @@
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
"license": "MIT",
"devDependencies": {
"@cloudflare/workers-types": "catalog:",
"@tsconfig/node22": "22.0.2",
+1
View File
@@ -3,6 +3,7 @@
"version": "1.0.224",
"name": "opencode",
"type": "module",
"license": "MIT",
"private": true,
"scripts": {
"typecheck": "tsgo --noEmit",
@@ -549,6 +549,13 @@ function App() {
})
})
sdk.event.on(TuiEvent.SessionSelect.type, (evt) => {
route.navigate({
type: "session",
sessionID: evt.properties.sessionID,
})
})
sdk.event.on(SessionApi.Event.Deleted.type, (evt) => {
if (route.data.type === "session" && route.data.sessionID === evt.properties.info.id) {
route.navigate({ type: "home" })
@@ -37,4 +37,10 @@ export const TuiEvent = {
duration: z.number().default(5000).optional().describe("Duration in milliseconds"),
}),
),
SessionSelect: BusEvent.define(
"tui.session.select",
z.object({
sessionID: z.string().regex(/^ses/).describe("Session ID to navigate to"),
}),
),
}
+2
View File
@@ -45,6 +45,8 @@ export const LANGUAGE_EXTENSIONS: Record<string, string> = {
".ini": "ini",
".java": "java",
".js": "javascript",
".kt": "kotlin",
".kts": "kotlin",
".jsx": "javascriptreact",
".json": "json",
".tex": "latex",
+79
View File
@@ -1209,6 +1209,85 @@ export namespace LSPServer {
},
}
export const KotlinLS: Info = {
id: "kotlin-ls",
extensions: [".kt", ".kts"],
root: NearestRoot(["build.gradle", "build.gradle.kts", "settings.gradle.kts", "pom.xml"]),
async spawn(root) {
const distPath = path.join(Global.Path.bin, "kotlin-ls")
const launcherScript =
process.platform === "win32" ? path.join(distPath, "kotlin-lsp.cmd") : path.join(distPath, "kotlin-lsp.sh")
const installed = await Bun.file(launcherScript).exists()
if (!installed) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("Downloading Kotlin Language Server from GitHub.")
const releaseResponse = await fetch("https://api.github.com/repos/Kotlin/kotlin-lsp/releases/latest")
if (!releaseResponse.ok) {
log.error("Failed to fetch kotlin-lsp release info")
return
}
const release = await releaseResponse.json()
const version = release.name?.replace(/^v/, "")
if (!version) {
log.error("Could not determine Kotlin LSP version from release")
return
}
const platform = process.platform
const arch = process.arch
let kotlinArch: string = arch
if (arch === "arm64") kotlinArch = "aarch64"
else if (arch === "x64") kotlinArch = "x64"
let kotlinPlatform: string = platform
if (platform === "darwin") kotlinPlatform = "mac"
else if (platform === "linux") kotlinPlatform = "linux"
else if (platform === "win32") kotlinPlatform = "win"
const supportedCombos = ["mac-x64", "mac-aarch64", "linux-x64", "linux-aarch64", "win-x64", "win-aarch64"]
const combo = `${kotlinPlatform}-${kotlinArch}`
if (!supportedCombos.includes(combo)) {
log.error(`Platform ${platform}/${arch} is not supported by Kotlin LSP`)
return
}
const assetName = `kotlin-lsp-${version}-${kotlinPlatform}-${kotlinArch}.zip`
const releaseURL = `https://download-cdn.jetbrains.com/kotlin-lsp/${version}/${assetName}`
await fs.mkdir(distPath, { recursive: true })
const archivePath = path.join(distPath, "kotlin-ls.zip")
await $`curl -L -o '${archivePath}' '${releaseURL}'`.quiet().nothrow()
const ok = await Archive.extractZip(archivePath, distPath)
.then(() => true)
.catch((error) => {
log.error("Failed to extract Kotlin LS archive", { error })
return false
})
if (!ok) return
await fs.rm(archivePath, { force: true })
if (process.platform !== "win32") {
await $`chmod +x ${launcherScript}`.quiet().nothrow()
}
log.info("Installed Kotlin Language Server", { path: launcherScript })
}
if (!(await Bun.file(launcherScript).exists())) {
log.error(`Failed to locate the Kotlin LS launcher script in the installed directory: ${distPath}.`)
return
}
return {
process: spawn(launcherScript, ["--stdio"], {
cwd: root,
}),
}
},
}
export const YamlLS: Info = {
id: "yaml-ls",
extensions: [".yaml", ".yml"],
+2 -1
View File
@@ -39,8 +39,9 @@ export namespace Plugin {
const lastAtIndex = plugin.lastIndexOf("@")
const pkg = lastAtIndex > 0 ? plugin.substring(0, lastAtIndex) : plugin
const version = lastAtIndex > 0 ? plugin.substring(lastAtIndex + 1) : "latest"
const builtin = BUILTIN.some((x) => x.startsWith(pkg + "@"))
plugin = await BunProc.install(pkg, version).catch((err) => {
if (BUILTIN.includes(pkg)) return ""
if (builtin) return ""
throw err
})
if (!plugin) continue
+27
View File
@@ -974,6 +974,7 @@ export namespace Server {
return c.json(true)
},
)
.post(
"/session/:sessionID/share",
describeRoute({
@@ -2600,6 +2601,32 @@ export namespace Server {
return c.json(true)
},
)
.post(
"/tui/select-session",
describeRoute({
summary: "Select session",
description: "Navigate the TUI to display the specified session.",
operationId: "tui.selectSession",
responses: {
200: {
description: "Session selected successfully",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(400, 404),
},
}),
validator("json", TuiEvent.SessionSelect.properties),
async (c) => {
const { sessionID } = c.req.valid("json")
await Session.get(sessionID)
await Bus.publish(TuiEvent.SessionSelect, { sessionID })
return c.json(true)
},
)
.route("/tui/control", TuiRoute)
.put(
"/auth/:providerID",
@@ -0,0 +1,78 @@
import { describe, expect, test } from "bun:test"
import path from "path"
import { Session } from "../../src/session"
import { Log } from "../../src/util/log"
import { Instance } from "../../src/project/instance"
import { Server } from "../../src/server/server"
const projectRoot = path.join(__dirname, "../..")
Log.init({ print: false })
describe("tui.selectSession endpoint", () => {
test("should return 200 when called with valid session", async () => {
await Instance.provide({
directory: projectRoot,
fn: async () => {
// #given
const session = await Session.create({})
// #when
const app = Server.App()
const response = await app.request("/tui/select-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionID: session.id }),
})
// #then
expect(response.status).toBe(200)
const body = await response.json()
expect(body).toBe(true)
await Session.remove(session.id)
},
})
})
test("should return 404 when session does not exist", async () => {
await Instance.provide({
directory: projectRoot,
fn: async () => {
// #given
const nonExistentSessionID = "ses_nonexistent123"
// #when
const app = Server.App()
const response = await app.request("/tui/select-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionID: nonExistentSessionID }),
})
// #then
expect(response.status).toBe(404)
},
})
})
test("should return 400 when session ID format is invalid", async () => {
await Instance.provide({
directory: projectRoot,
fn: async () => {
// #given
const invalidSessionID = "invalid_session_id"
// #when
const app = Server.App()
const response = await app.request("/tui/select-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionID: invalidSessionID }),
})
// #then
expect(response.status).toBe(400)
},
})
})
})
+1
View File
@@ -3,6 +3,7 @@
"name": "@opencode-ai/plugin",
"version": "1.0.224",
"type": "module",
"license": "MIT",
"scripts": {
"typecheck": "tsgo --noEmit",
"build": "tsc"
+1
View File
@@ -1,6 +1,7 @@
{
"$schema": "https://json.schemastore.org/package",
"name": "@opencode-ai/script",
"license": "MIT",
"devDependencies": {
"@types/bun": "catalog:"
},
+1
View File
@@ -3,6 +3,7 @@
"name": "@opencode-ai/sdk",
"version": "1.0.224",
"type": "module",
"license": "MIT",
"scripts": {
"typecheck": "tsgo --noEmit",
"build": "./script/build.ts"
+39 -1
View File
@@ -19,6 +19,7 @@ import type {
EventSubscribeResponses,
EventTuiCommandExecute,
EventTuiPromptAppend,
EventTuiSessionSelect,
EventTuiToastShow,
FileListResponses,
FilePartInput,
@@ -144,6 +145,8 @@ import type {
TuiOpenThemesResponses,
TuiPublishErrors,
TuiPublishResponses,
TuiSelectSessionErrors,
TuiSelectSessionResponses,
TuiShowToastResponses,
TuiSubmitPromptResponses,
VcsGetResponses,
@@ -2688,7 +2691,7 @@ export class Tui extends HeyApiClient {
public publish<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow
body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow | EventTuiSessionSelect
},
options?: Options<never, ThrowOnError>,
) {
@@ -2705,6 +2708,41 @@ export class Tui extends HeyApiClient {
})
}
/**
* Select session
*
* Navigate the TUI to display the specified session.
*/
public selectSession<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
sessionID?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "body", key: "sessionID" },
],
},
],
)
return (options?.client ?? this.client).post<TuiSelectSessionResponses, TuiSelectSessionErrors, ThrowOnError>({
url: "/tui/select-session",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
control = new Control({ client: this.client })
}
+48 -1
View File
@@ -592,6 +592,16 @@ export type EventTuiToastShow = {
}
}
export type EventTuiSessionSelect = {
type: "tui.session.select"
properties: {
/**
* Session ID to navigate to
*/
sessionID: string
}
}
export type EventMcpToolsChanged = {
type: "mcp.tools.changed"
properties: {
@@ -776,6 +786,7 @@ export type Event =
| EventTuiPromptAppend
| EventTuiCommandExecute
| EventTuiToastShow
| EventTuiSessionSelect
| EventMcpToolsChanged
| EventCommandExecuted
| EventSessionCreated
@@ -4310,7 +4321,7 @@ export type TuiShowToastResponses = {
export type TuiShowToastResponse = TuiShowToastResponses[keyof TuiShowToastResponses]
export type TuiPublishData = {
body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow
body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow | EventTuiSessionSelect
path?: never
query?: {
directory?: string
@@ -4336,6 +4347,42 @@ export type TuiPublishResponses = {
export type TuiPublishResponse = TuiPublishResponses[keyof TuiPublishResponses]
export type TuiSelectSessionData = {
body?: {
/**
* Session ID to navigate to
*/
sessionID: string
}
path?: never
query?: {
directory?: string
}
url: "/tui/select-session"
}
export type TuiSelectSessionErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type TuiSelectSessionError = TuiSelectSessionErrors[keyof TuiSelectSessionErrors]
export type TuiSelectSessionResponses = {
/**
* Session selected successfully
*/
200: boolean
}
export type TuiSelectSessionResponse = TuiSelectSessionResponses[keyof TuiSelectSessionResponses]
export type TuiControlNextData = {
body?: never
path?: never
+98
View File
@@ -4956,6 +4956,9 @@
},
{
"$ref": "#/components/schemas/Event.tui.toast.show"
},
{
"$ref": "#/components/schemas/Event.tui.session.select"
}
]
}
@@ -4970,6 +4973,77 @@
]
}
},
"/tui/select-session": {
"post": {
"operationId": "tui.selectSession",
"parameters": [
{
"in": "query",
"name": "directory",
"schema": {
"type": "string"
}
}
],
"summary": "Select session",
"description": "Navigate the TUI to display the specified session.",
"responses": {
"200": {
"description": "Session selected successfully",
"content": {
"application/json": {
"schema": {
"type": "boolean"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NotFoundError"
}
}
}
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"sessionID": {
"description": "Session ID to navigate to",
"type": "string",
"pattern": "^ses"
}
},
"required": ["sessionID"]
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.selectSession({\n ...\n})"
}
]
}
},
"/tui/control/next": {
"get": {
"operationId": "tui.control.next",
@@ -6794,6 +6868,27 @@
},
"required": ["type", "properties"]
},
"Event.tui.session.select": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "tui.session.select"
},
"properties": {
"type": "object",
"properties": {
"sessionID": {
"description": "Session ID to navigate to",
"type": "string",
"pattern": "^ses"
}
},
"required": ["sessionID"]
}
},
"required": ["type", "properties"]
},
"Event.mcp.tools.changed": {
"type": "object",
"properties": {
@@ -7338,6 +7433,9 @@
{
"$ref": "#/components/schemas/Event.tui.toast.show"
},
{
"$ref": "#/components/schemas/Event.tui.session.select"
},
{
"$ref": "#/components/schemas/Event.mcp.tools.changed"
},
+1
View File
@@ -2,6 +2,7 @@
"name": "@opencode-ai/slack",
"version": "1.0.224",
"type": "module",
"license": "MIT",
"scripts": {
"dev": "bun run src/index.ts",
"typecheck": "tsgo --noEmit"
+1
View File
@@ -2,6 +2,7 @@
"name": "@opencode-ai/ui",
"version": "1.0.224",
"type": "module",
"license": "MIT",
"exports": {
"./*": "./src/components/*.tsx",
"./pierre": "./src/pierre/index.ts",
+1
View File
@@ -3,6 +3,7 @@
"version": "1.0.224",
"private": true,
"type": "module",
"license": "MIT",
"exports": {
"./*": "./src/*.ts"
},
+1
View File
@@ -1,6 +1,7 @@
{
"name": "@opencode-ai/web",
"type": "module",
"license": "MIT",
"version": "1.0.224",
"scripts": {
"dev": "astro dev",
@@ -38,6 +38,7 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw
| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Allow OpenCode agents to lazy load prompts on demand with skill discovery and injection |
| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Persistent memory across sessions using Supermemory |
| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Interactive plan review with visual annotation and private/offline sharing |
| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | Extend opencode /commands into a powerful orchestration system with granular flow control |
---
+1
View File
@@ -26,6 +26,7 @@ OpenCode comes with several built-in LSP servers for popular languages:
| gleam | .gleam | `gleam` command available |
| gopls | .go | `go` command available |
| jdtls | .java | `Java SDK (version 21+)` installed |
| kotlin-ls | .kt, .kts | Auto-installs for Kotlin projects |
| lua-ls | .lua | Auto-installs for Lua projects |
| nixd | .nix | `nixd` command available |
| ocaml-lsp | .ml, .mli | `ocamllsp` command available |