diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx
index b85cd5c6542..e42434aa7b2 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx
@@ -157,6 +157,28 @@ export function DialogStatus() {
+ 0} fallback={No Skills}>
+
+ {sync.data.skill.length} Skills
+
+ {(item) => (
+
+
+ •
+
+
+ {item.name}
+
+
+ )}
+
+
+
)
}
diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx
index 2528a499896..7f031b40763 100644
--- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx
@@ -16,6 +16,7 @@ import type {
ProviderAuthMethod,
VcsInfo,
} from "@opencode-ai/sdk/v2"
+import { Skill } from "@/skill"
import { createStore, produce, reconcile } from "solid-js/store"
import { useSDK } from "@tui/context/sdk"
import { Binary } from "@opencode-ai/util/binary"
@@ -63,6 +64,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
[key: string]: McpStatus
}
formatter: FormatterStatus[]
+ skill: Skill.Info[]
vcs: VcsInfo | undefined
path: Path
}>({
@@ -88,6 +90,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
lsp: [],
mcp: {},
formatter: [],
+ skill: [],
vcs: undefined,
path: { state: "", config: "", worktree: "", directory: "" },
})
@@ -293,6 +296,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
sdk.client.lsp.status().then((x) => setStore("lsp", x.data!)),
sdk.client.mcp.status().then((x) => setStore("mcp", x.data!)),
sdk.client.formatter.status().then((x) => setStore("formatter", x.data!)),
+ sdk.client.skill.status().then((x) => setStore("skill", x.data!)),
sdk.client.session.status().then((x) => setStore("session_status", x.data!)),
sdk.client.provider.auth().then((x) => setStore("provider_auth", x.data ?? {})),
sdk.client.vcs.get().then((x) => setStore("vcs", x.data)),
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx
index 69082c870ba..fd621c90592 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx
@@ -79,6 +79,11 @@ export function Footer() {
{mcp()} MCP
+ 0}>
+
+ • {sync.data.skill.length} Skills
+
+
/status
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
index a9ed042d1bb..e951efea42b 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
@@ -25,11 +25,14 @@ export function Sidebar(props: { sessionID: string }) {
diff: true,
todo: true,
lsp: true,
+ skill: true,
})
// Sort MCP servers alphabetically for consistent display order
const mcpEntries = createMemo(() => Object.entries(sync.data.mcp).sort(([a], [b]) => a.localeCompare(b)))
+ const skillEntries = createMemo(() => sync.data.skill.toSorted((a, b) => a.name.localeCompare(b.name)))
+
// Count connected and error MCP servers for collapsed header display
const connectedMcpCount = createMemo(() => mcpEntries().filter(([_, item]) => item.status === "connected").length)
const errorMcpCount = createMemo(
@@ -200,6 +203,34 @@ export function Sidebar(props: { sessionID: string }) {
+ 0}>
+
+ skillEntries().length > 2 && setExpanded("skill", !expanded.skill)}
+ >
+ 2}>
+ {expanded.skill ? "▼" : "▶"}
+
+
+ Skills
+
+
+
+
+ {(item) => (
+
+
+ •
+
+ {item.name}
+
+ )}
+
+
+
+
0 && todo().some((t) => t.status !== "completed")}>
{
+ return c.json(await Skill.status())
+ },
+ )
.post(
"/tui/append-prompt",
describeRoute({
diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts
index 41df88f8b6a..623dd9234cf 100644
--- a/packages/opencode/src/skill/skill.ts
+++ b/packages/opencode/src/skill/skill.ts
@@ -80,4 +80,8 @@ export namespace Skill {
export async function all() {
return state().then((x) => Object.values(x))
}
+
+ export async function status() {
+ return all()
+ }
}
diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts
index 97bc92b8669..84a86843265 100644
--- a/packages/sdk/js/src/v2/gen/sdk.gen.ts
+++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts
@@ -121,6 +121,7 @@ import type {
SessionUnshareResponses,
SessionUpdateErrors,
SessionUpdateResponses,
+ SkillStatusResponses,
SubtaskPartInput,
TextPartInput,
ToolIdsErrors,
@@ -2334,6 +2335,27 @@ export class Formatter extends HeyApiClient {
}
}
+export class Skill extends HeyApiClient {
+ /**
+ * Get skill status
+ *
+ * Get all loaded skills
+ */
+ public status(
+ parameters?: {
+ directory?: string
+ },
+ options?: Options,
+ ) {
+ const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }] }])
+ return (options?.client ?? this.client).get({
+ url: "/skill",
+ ...options,
+ ...params,
+ })
+ }
+}
+
export class Control extends HeyApiClient {
/**
* Get next TUI request
@@ -2701,6 +2723,8 @@ export class OpencodeClient extends HeyApiClient {
formatter = new Formatter({ client: this.client })
+ skill = new Skill({ client: this.client })
+
tui = new Tui({ client: this.client })
auth = new Auth({ client: this.client })
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index 0a31394ed9c..943e8643f2d 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -4000,6 +4000,28 @@ export type FormatterStatusResponses = {
export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses]
+export type SkillStatusData = {
+ body?: never
+ path?: never
+ query?: {
+ directory?: string
+ }
+ url: "/skill"
+}
+
+export type SkillStatusResponses = {
+ /**
+ * List of loaded skills
+ */
+ 200: Array<{
+ name: string
+ description: string
+ location: string
+ }>
+}
+
+export type SkillStatusResponse = SkillStatusResponses[keyof SkillStatusResponses]
+
export type TuiAppendPromptData = {
body?: {
text: string