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 0edc911344c..201f6fc9fcd 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -18,6 +18,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" @@ -71,6 +72,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ [key: string]: McpResource } formatter: FormatterStatus[] + skill: Skill.Info[] vcs: VcsInfo | undefined path: Path }>({ @@ -98,6 +100,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ mcp: {}, mcp_resource: {}, formatter: [], + skill: [], vcs: undefined, path: { state: "", config: "", worktree: "", directory: "" }, }) @@ -346,6 +349,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ sdk.client.mcp.status().then((x) => setStore("mcp", reconcile(x.data!))), sdk.client.experimental.resource.list().then((x) => setStore("mcp_resource", reconcile(x.data ?? {}))), sdk.client.formatter.status().then((x) => setStore("formatter", reconcile(x.data!))), + sdk.client.skill.status().then((x) => setStore("skill", reconcile(x.data!))), sdk.client.session.status().then((x) => { setStore("session_status", reconcile(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 d10c49c833f..a5d7567f916 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 c0f4bd74abd..e3ae1956721 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; overlay?: boolean }) { 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( @@ -201,6 +204,34 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { + 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 cbc042d1e96..5be95eb3694 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -124,4 +124,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 f83913ea5e1..2fdda7f0651 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -134,6 +134,7 @@ import type { SessionUnshareResponses, SessionUpdateErrors, SessionUpdateResponses, + SkillStatusResponses, SubtaskPartInput, TextPartInput, ToolIdsErrors, @@ -2618,6 +2619,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 @@ -3026,6 +3048,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 9cb7222aa5f..0bd2ebcb646 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -4412,6 +4412,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