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