From 208f0101bcb45079fa4c4bb54cf8b8bfe94b84f4 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Sun, 4 Jan 2026 12:52:50 +0000 Subject: [PATCH 1/7] add foldable available skills section to sidebar --- packages/app/src/context/global-sync.tsx | 7 +++++ packages/app/src/pages/layout.tsx | 30 +++++++++++++++++++ .../opencode/src/cli/cmd/tui/context/sync.tsx | 3 ++ .../cli/cmd/tui/routes/session/sidebar.tsx | 26 ++++++++++++++++ packages/opencode/src/server/server.ts | 2 ++ packages/opencode/src/server/skill.ts | 27 +++++++++++++++++ packages/sdk/js/src/v2/gen/sdk.gen.ts | 24 +++++++++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 22 ++++++++++++++ 8 files changed, 141 insertions(+) create mode 100644 packages/opencode/src/server/skill.ts diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 913e54d1065..293402535cb 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -69,12 +69,14 @@ function createGlobalSync() { error?: InitError path: Path project: Project[] + skill: { name: string; description: string; location: string }[] provider: ProviderListResponse provider_auth: ProviderAuthResponse }>({ ready: false, path: { state: "", config: "", worktree: "", directory: "", home: "" }, project: [], + skill: [], provider: { all: [], connected: [], default: {} }, provider_auth: {}, }) @@ -434,6 +436,11 @@ function createGlobalSync() { setGlobalStore("project", projects) }), ), + retry(() => + globalSDK.client.skill.list().then((x) => { + setGlobalStore("skill", x.data ?? []) + }), + ), retry(() => globalSDK.client.provider.list().then((x) => { const data = x.data! diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 91df9259ef7..2c1cd849420 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -63,6 +63,7 @@ export default function Layout(props: ParentProps) { lastSession: {} as { [directory: string]: string }, activeDraggable: undefined as string | undefined, mobileProjectsExpanded: {} as Record, + skillsExpanded: true, }) const mobileProjects = { @@ -964,6 +965,35 @@ export default function Layout(props: ParentProps) { + 0}> +
+ + +
+ + {(skill) => ( + +
+ {skill.name} +
+
+ )} +
+
+
+
+
diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 893cc10ad9b..170e854ec4c 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -59,6 +59,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ [messageID: string]: Part[] } lsp: LspStatus[] + skill: { name: string; description: string; location: string }[] mcp: { [key: string]: McpStatus } @@ -86,6 +87,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ message: {}, part: {}, lsp: [], + skill: [], mcp: {}, formatter: [], vcs: undefined, @@ -294,6 +296,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ ...(args.continue ? [] : [sessionListPromise]), sdk.client.command.list().then((x) => setStore("command", reconcile(x.data ?? []))), sdk.client.lsp.status().then((x) => setStore("lsp", reconcile(x.data!))), + sdk.client.skill.list().then((x) => setStore("skill", reconcile(x.data ?? []))), sdk.client.mcp.status().then((x) => setStore("mcp", reconcile(x.data!))), sdk.client.formatter.status().then((x) => setStore("formatter", reconcile(x.data!))), sdk.client.session.status().then((x) => { 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..fb8755d0219 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -24,6 +24,7 @@ export function Sidebar(props: { sessionID: string }) { mcp: true, diff: true, todo: true, + skill: true, lsp: true, }) @@ -157,6 +158,31 @@ export function Sidebar(props: { sessionID: string }) { + 0}> + + sync.data.skill.length > 2 && setExpanded("skill", !expanded.skill)} + > + 2}> + {expanded.skill ? "▼" : "▶"} + + + Skills + + + + + {(skill) => ( + + • {skill.name} + + )} + + + + { + const skills = await Skill.all() + return c.json(skills) + }, +) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 01de8c183eb..928033fbe54 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -126,6 +126,7 @@ import type { SessionUnshareResponses, SessionUpdateErrors, SessionUpdateResponses, + SkillListResponses, SubtaskPartInput, TextPartInput, ToolIdsErrors, @@ -319,6 +320,27 @@ export class Project extends HeyApiClient { } } +export class Skill extends HeyApiClient { + /** + * List all skills + * + * Get a list of all available skills. + */ + public list( + 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 Pty extends HeyApiClient { /** * List PTY sessions @@ -2841,6 +2863,8 @@ export class OpencodeClient extends HeyApiClient { project = new Project({ client: this.client }) + skill = new Skill({ client: this.client }) + pty = new Pty({ client: this.client }) config = new Config({ 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 b46a9bd3b94..59a04fe55c4 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2113,6 +2113,28 @@ export type ProjectUpdateResponses = { export type ProjectUpdateResponse = ProjectUpdateResponses[keyof ProjectUpdateResponses] +export type SkillListData = { + body?: never + path?: never + query?: { + directory?: string + } + url: "/skill" +} + +export type SkillListResponses = { + /** + * List of skills + */ + 200: Array<{ + name: string + description: string + location: string + }> +} + +export type SkillListResponse = SkillListResponses[keyof SkillListResponses] + export type PtyListData = { body?: never path?: never From 0812531150cc8c8397b8594d19eb93a9ec30c108 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Sun, 4 Jan 2026 12:58:32 +0000 Subject: [PATCH 2/7] update: filter skills in sidebar based on agent permissions --- packages/app/src/context/global-sync.tsx | 14 ++++++++ packages/app/src/pages/layout.tsx | 6 +++- .../opencode/src/cli/cmd/tui/context/sync.tsx | 4 ++- packages/opencode/src/server/skill.ts | 34 ++++++++++++++++--- packages/sdk/js/src/v2/gen/sdk.gen.ts | 17 ++++++++-- packages/sdk/js/src/v2/gen/types.gen.ts | 1 + 6 files changed, 67 insertions(+), 9 deletions(-) diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 293402535cb..3d7a9fc1f66 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -136,6 +136,17 @@ function createGlobalSync() { }) } + async function loadSkills(sessionID?: string) { + globalSDK.client.skill + .list({ sessionID }) + .then((x) => { + setGlobalStore("skill", reconcile(x.data ?? [])) + }) + .catch((err) => { + console.error("Failed to load skills", err) + }) + } + async function bootstrapInstance(directory: string) { if (!directory) return const [store, setStore] = child(directory) @@ -479,6 +490,9 @@ function createGlobalSync() { }, child, bootstrap, + skill: { + load: loadSkills, + }, project: { loadSessions, }, diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 2c1cd849420..a38517c549b 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -510,13 +510,17 @@ export default function Layout(props: ParentProps) { } createEffect(() => { - if (!params.dir || !params.id) return + if (!params.dir || !params.id) { + globalSync.skill.load() + return + } const directory = base64Decode(params.dir) const id = params.id setStore("lastSession", directory, id) notification.session.markViewed(id) untrack(() => layout.projects.expand(directory)) requestAnimationFrame(() => scrollToSession(id)) + globalSync.skill.load(id) }) createEffect(() => { diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 170e854ec4c..b408a1d6944 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -351,11 +351,12 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ }, async sync(sessionID: string) { if (fullSyncedSessions.has(sessionID)) return - const [session, messages, todo, diff] = await Promise.all([ + const [session, messages, todo, diff, skills] = await Promise.all([ sdk.client.session.get({ sessionID }, { throwOnError: true }), sdk.client.session.messages({ sessionID, limit: 100 }), sdk.client.session.todo({ sessionID }), sdk.client.session.diff({ sessionID }), + sdk.client.skill.list({ sessionID }), ]) setStore( produce((draft) => { @@ -368,6 +369,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ draft.part[message.info.id] = message.parts } draft.session_diff[sessionID] = diff.data ?? [] + draft.skill = skills.data ?? [] }), ) fullSyncedSessions.add(sessionID) diff --git a/packages/opencode/src/server/skill.ts b/packages/opencode/src/server/skill.ts index da3da030b46..53780389a45 100644 --- a/packages/opencode/src/server/skill.ts +++ b/packages/opencode/src/server/skill.ts @@ -1,13 +1,18 @@ import { Hono } from "hono" import { describeRoute } from "hono-openapi" -import { resolver } from "hono-openapi" +import { resolver, validator } from "hono-openapi" import { Skill } from "../skill/skill" +import { Session } from "../session" +import { Agent } from "../agent/agent" +import { PermissionNext } from "@/permission/next" +import { MessageV2 } from "../session/message-v2" +import z from "zod" export const SkillRoute = new Hono().get( "/", describeRoute({ - summary: "List all skills", - description: "Get a list of all available skills.", + summary: "List available skills", + description: "Get a list of skills available for a specific session.", operationId: "skill.list", responses: { 200: { @@ -20,8 +25,29 @@ export const SkillRoute = new Hono().get( }, }, }), + validator("query", z.object({ sessionID: z.string().optional() })), async (c) => { + const { sessionID } = c.req.valid("query") const skills = await Skill.all() - return c.json(skills) + if (!sessionID) return c.json(skills) + + const session = await Session.get(sessionID) + if (!session) return c.json(skills) + + const messages = await Session.messages({ sessionID, limit: 10 }) + const lastUser = messages.find((m) => m.info.role === "user")?.info as MessageV2.User | undefined + const agentName = lastUser?.agent ?? (await Agent.defaultAgent()) + const agent = await Agent.get(agentName) + + if (!agent) return c.json(skills) + + const ruleset = PermissionNext.merge(agent.permission, session.permission ?? []) + + const filtered = skills.filter((skill) => { + const rule = PermissionNext.evaluate("skill", skill.name, ruleset) + return rule.action !== "deny" + }) + + return c.json(filtered) }, ) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 928033fbe54..6cc86048d12 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -322,17 +322,28 @@ export class Project extends HeyApiClient { export class Skill extends HeyApiClient { /** - * List all skills + * List available skills * - * Get a list of all available skills. + * Get a list of skills available for a specific session. */ public list( parameters?: { directory?: string + sessionID?: string }, options?: Options, ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }] }]) + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "sessionID" }, + ], + }, + ], + ) return (options?.client ?? this.client).get({ url: "/skill", ...options, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 59a04fe55c4..8f77033c46e 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2118,6 +2118,7 @@ export type SkillListData = { path?: never query?: { directory?: string + sessionID?: string } url: "/skill" } From e9c3497eeda14f7faa276538f6c93c5f66635053 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Sun, 4 Jan 2026 13:04:01 +0000 Subject: [PATCH 3/7] fix: implement robust skill filtering with frontmatter allow/deny and correct directory scoping --- packages/app/src/context/global-sync.tsx | 4 ++-- packages/app/src/pages/layout.tsx | 5 +++-- packages/opencode/src/server/skill.ts | 15 ++++++++++++++- packages/opencode/src/skill/skill.ts | 13 ++++++++++--- packages/sdk/js/src/v2/gen/types.gen.ts | 2 ++ 5 files changed, 31 insertions(+), 8 deletions(-) diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 3d7a9fc1f66..cf5f695ab08 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -136,9 +136,9 @@ function createGlobalSync() { }) } - async function loadSkills(sessionID?: string) { + async function loadSkills(directory?: string, sessionID?: string) { globalSDK.client.skill - .list({ sessionID }) + .list({ directory, sessionID }) .then((x) => { setGlobalStore("skill", reconcile(x.data ?? [])) }) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index a38517c549b..2a7a935b7f2 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -511,7 +511,8 @@ export default function Layout(props: ParentProps) { createEffect(() => { if (!params.dir || !params.id) { - globalSync.skill.load() + const directory = params.dir ? base64Decode(params.dir) : undefined + globalSync.skill.load(directory) return } const directory = base64Decode(params.dir) @@ -520,7 +521,7 @@ export default function Layout(props: ParentProps) { notification.session.markViewed(id) untrack(() => layout.projects.expand(directory)) requestAnimationFrame(() => scrollToSession(id)) - globalSync.skill.load(id) + globalSync.skill.load(directory, id) }) createEffect(() => { diff --git a/packages/opencode/src/server/skill.ts b/packages/opencode/src/server/skill.ts index 53780389a45..69011805dd0 100644 --- a/packages/opencode/src/server/skill.ts +++ b/packages/opencode/src/server/skill.ts @@ -4,6 +4,7 @@ import { resolver, validator } from "hono-openapi" import { Skill } from "../skill/skill" import { Session } from "../session" import { Agent } from "../agent/agent" +import { Config } from "../config/config" import { PermissionNext } from "@/permission/next" import { MessageV2 } from "../session/message-v2" import z from "zod" @@ -29,7 +30,16 @@ export const SkillRoute = new Hono().get( async (c) => { const { sessionID } = c.req.valid("query") const skills = await Skill.all() - if (!sessionID) return c.json(skills) + const cfg = await Config.get() + + if (!sessionID) { + const globalRuleset = PermissionNext.fromConfig(cfg.permission ?? {}) + const filtered = skills.filter((skill) => { + const rule = PermissionNext.evaluate("skill", skill.name, globalRuleset) + return rule.action !== "deny" + }) + return c.json(filtered) + } const session = await Session.get(sessionID) if (!session) return c.json(skills) @@ -44,6 +54,9 @@ export const SkillRoute = new Hono().get( const ruleset = PermissionNext.merge(agent.permission, session.permission ?? []) const filtered = skills.filter((skill) => { + if (skill.deny?.includes(agentName)) return false + if (skill.allow?.length && !skill.allow.includes(agentName)) return false + const rule = PermissionNext.evaluate("skill", skill.name, ruleset) return rule.action !== "deny" }) diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index bf90dd5870c..26a7178cf95 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -14,6 +14,14 @@ export namespace Skill { name: z.string(), description: z.string(), location: z.string(), + allow: z + .union([z.string(), z.array(z.string())]) + .optional() + .transform((x) => (typeof x === "string" ? [x] : x)), + deny: z + .union([z.string(), z.array(z.string())]) + .optional() + .transform((x) => (typeof x === "string" ? [x] : x)), }) export type Info = z.infer @@ -47,7 +55,7 @@ export namespace Skill { return } - const parsed = Info.pick({ name: true, description: true }).safeParse(md.data) + const parsed = Info.omit({ location: true }).safeParse(md.data) if (!parsed.success) return // Warn on duplicate skill names @@ -60,8 +68,7 @@ export namespace Skill { } skills[parsed.data.name] = { - name: parsed.data.name, - description: parsed.data.description, + ...parsed.data, location: match, } } diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 8f77033c46e..282d094374d 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2131,6 +2131,8 @@ export type SkillListResponses = { name: string description: string location: string + allow?: string | Array + deny?: string | Array }> } From 96d9c7bfa19acb136ad2aecec891e14f4b351b2a Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Sun, 4 Jan 2026 13:18:51 +0000 Subject: [PATCH 4/7] fix: implement per-currently-selected-agent skill filtering in sidebar --- packages/app/src/components/prompt-input.tsx | 4 +- packages/app/src/context/global-sync.tsx | 12 ++- packages/app/src/context/local.tsx | 83 ++++--------------- packages/app/src/pages/layout.tsx | 11 +-- .../src/cli/cmd/tui/routes/session/index.tsx | 11 +++ packages/opencode/src/server/skill.ts | 46 +++++----- packages/sdk/js/src/v2/gen/sdk.gen.ts | 2 + packages/sdk/js/src/v2/gen/types.gen.ts | 1 + 8 files changed, 69 insertions(+), 101 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 9657507ffb3..936caa7724e 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1449,7 +1449,7 @@ export const PromptInput: Component = (props) => { } return ( -
+
local.model.variant.cycle()} > {local.model.variant.current() ?? "Default"} diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index cf5f695ab08..3e17cc92577 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -72,6 +72,7 @@ function createGlobalSync() { skill: { name: string; description: string; location: string }[] provider: ProviderListResponse provider_auth: ProviderAuthResponse + selected_agent: Record }>({ ready: false, path: { state: "", config: "", worktree: "", directory: "", home: "" }, @@ -79,6 +80,7 @@ function createGlobalSync() { skill: [], provider: { all: [], connected: [], default: {} }, provider_auth: {}, + selected_agent: {}, }) const children: Record>> = {} @@ -136,9 +138,9 @@ function createGlobalSync() { }) } - async function loadSkills(directory?: string, sessionID?: string) { + async function loadSkills(directory?: string, sessionID?: string, agent?: string) { globalSDK.client.skill - .list({ directory, sessionID }) + .list({ directory, sessionID, agent }) .then((x) => { setGlobalStore("skill", reconcile(x.data ?? [])) }) @@ -147,6 +149,11 @@ function createGlobalSync() { }) } + function setSelectedAgent(directory: string, agent: string) { + setGlobalStore("selected_agent", directory, agent) + loadSkills(directory, undefined, agent) + } + async function bootstrapInstance(directory: string) { if (!directory) return const [store, setStore] = child(directory) @@ -492,6 +499,7 @@ function createGlobalSync() { bootstrap, skill: { load: loadSkills, + setSelectedAgent, }, project: { loadSessions, diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx index 5a4f0fa0dc0..6f46779805a 100644 --- a/packages/app/src/context/local.tsx +++ b/packages/app/src/context/local.tsx @@ -1,10 +1,12 @@ import { createStore, produce, reconcile } from "solid-js/store" -import { batch, createMemo } from "solid-js" +import { batch, createMemo, createEffect } from "solid-js" +import { useParams } from "@solidjs/router" import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda" import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2" import { createSimpleContext } from "@opencode-ai/ui/context" import { useSDK } from "./sdk" import { useSync } from "./sync" +import { useGlobalSync } from "./global-sync" import { base64Encode } from "@opencode-ai/util/encode" import { useProviders } from "@/hooks/use-providers" import { DateTime } from "luxon" @@ -42,6 +44,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const sdk = useSDK() const sync = useSync() const providers = useProviders() + const globalSync = useGlobalSync() + const params = useParams() function isModelValid(model: ModelKey) { const provider = providers.all().find((x) => x.id === model.providerID) @@ -69,6 +73,15 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }>({ current: list()[0]?.name, }) + + createEffect(() => { + const name = store.current + if (name) { + globalSync.skill.setSelectedAgent(sdk.directory, name) + globalSync.skill.load(sdk.directory, params.id, name) + } + }) + return { list, current() { @@ -320,62 +333,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const [store, setStore] = createStore<{ node: Record }>({ - node: {}, // Object.fromEntries(sync.data.node.map((x) => [x.path, x])), + node: {}, }) - // const changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path))) - // const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b))) - - // createEffect((prev: FileStatus[]) => { - // const removed = prev.filter((p) => !sync.data.changes.find((c) => c.path === p.path)) - // for (const p of removed) { - // setStore( - // "node", - // p.path, - // produce((draft) => { - // draft.status = undefined - // draft.view = "raw" - // }), - // ) - // load(p.path) - // } - // for (const p of sync.data.changes) { - // if (store.node[p.path] === undefined) { - // fetch(p.path).then(() => { - // if (store.node[p.path] === undefined) return - // setStore("node", p.path, "status", p) - // }) - // } else { - // setStore("node", p.path, "status", p) - // } - // } - // return sync.data.changes - // }, sync.data.changes) - - // const changed = (path: string) => { - // const node = store.node[path] - // if (node?.status) return true - // const set = changeset() - // if (set.has(path)) return true - // for (const p of set) { - // if (p.startsWith(path ? path + "/" : "")) return true - // } - // return false - // } - - // const resetNode = (path: string) => { - // setStore("node", path, { - // loaded: undefined, - // pinned: undefined, - // content: undefined, - // selection: undefined, - // scrollTop: undefined, - // folded: undefined, - // view: undefined, - // selectedChange: undefined, - // }) - // } - const relative = (path: string) => path.replace(sync.data.path.directory + "/", "") const load = async (path: string) => { @@ -420,17 +380,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const open = async (path: string, options?: { pinned?: boolean; view?: LocalFile["view"] }) => { const relativePath = relative(path) if (!store.node[relativePath]) await fetch(path) - // setStore("opened", (x) => { - // if (x.includes(relativePath)) return x - // return [ - // ...opened() - // .filter((x) => x.pinned) - // .map((x) => x.path), - // relativePath, - // ] - // }) - // setStore("active", relativePath) - // context.addActive() if (options?.pinned) setStore("node", path, "pinned", true) if (options?.view && store.node[relativePath].view === undefined) setStore("node", path, "view", options.view) if (store.node[relativePath]?.loaded) return @@ -522,8 +471,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ setChangeIndex(path: string, index: number | undefined) { setStore("node", path, "selectedChange", index) }, - // changes, - // changed, children(path: string) { return Object.values(store.node).filter( (x) => diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 2a7a935b7f2..ea5f8e21d27 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -510,18 +510,15 @@ export default function Layout(props: ParentProps) { } createEffect(() => { - if (!params.dir || !params.id) { - const directory = params.dir ? base64Decode(params.dir) : undefined - globalSync.skill.load(directory) - return - } - const directory = base64Decode(params.dir) + const directory = params.dir ? base64Decode(params.dir) : undefined const id = params.id + + if (!directory || !id) return + setStore("lastSession", directory, id) notification.session.markViewed(id) untrack(() => layout.projects.expand(directory)) requestAnimationFrame(() => scrollToSession(id)) - globalSync.skill.load(directory, id) }) createEffect(() => { diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index d049ec4373c..7720e23dcc4 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -9,7 +9,9 @@ import { Show, Switch, useContext, + batch, } from "solid-js" +import { reconcile } from "solid-js/store" import { Dynamic } from "solid-js/web" import path from "path" import { useRoute, useRouteData } from "@tui/context/route" @@ -245,6 +247,15 @@ export function Session() { const local = useLocal() + createEffect(() => { + const agent = local.agent.current()?.name + if (agent) { + sdk.client.skill.list({ sessionID: route.sessionID, agent }).then((x) => { + sync.set("skill", reconcile(x.data ?? [])) + }) + } + }) + function moveChild(direction: number) { if (children().length === 1) return let next = children().findIndex((x) => x.id === session()?.id) + direction diff --git a/packages/opencode/src/server/skill.ts b/packages/opencode/src/server/skill.ts index 69011805dd0..32f798f8431 100644 --- a/packages/opencode/src/server/skill.ts +++ b/packages/opencode/src/server/skill.ts @@ -26,39 +26,41 @@ export const SkillRoute = new Hono().get( }, }, }), - validator("query", z.object({ sessionID: z.string().optional() })), + validator("query", z.object({ sessionID: z.string().optional(), agent: z.string().optional() })), async (c) => { - const { sessionID } = c.req.valid("query") + const { sessionID, agent: agentParam } = c.req.valid("query") const skills = await Skill.all() const cfg = await Config.get() - if (!sessionID) { - const globalRuleset = PermissionNext.fromConfig(cfg.permission ?? {}) - const filtered = skills.filter((skill) => { - const rule = PermissionNext.evaluate("skill", skill.name, globalRuleset) - return rule.action !== "deny" - }) - return c.json(filtered) - } + const agentName = + (agentParam && agentParam !== "undefined" && agentParam !== "" ? agentParam : undefined) ?? + (await (async () => { + if (!sessionID) return undefined + const messages = await Session.messages({ sessionID, limit: 10 }) + return messages.findLast((m) => m.info.role === "user")?.info.agent + })()) ?? + (await Agent.defaultAgent()) - const session = await Session.get(sessionID) - if (!session) return c.json(skills) - - const messages = await Session.messages({ sessionID, limit: 10 }) - const lastUser = messages.find((m) => m.info.role === "user")?.info as MessageV2.User | undefined - const agentName = lastUser?.agent ?? (await Agent.defaultAgent()) const agent = await Agent.get(agentName) - if (!agent) return c.json(skills) - const ruleset = PermissionNext.merge(agent.permission, session.permission ?? []) + const session = sessionID ? await Session.get(sessionID) : undefined + const ruleset = PermissionNext.merge(agent.permission, session?.permission ?? []) const filtered = skills.filter((skill) => { - if (skill.deny?.includes(agentName)) return false - if (skill.allow?.length && !skill.allow.includes(agentName)) return false + // Check frontmatter allow/deny (case-insensitive) + const lowerAgentName = agentName.toLowerCase() + if (skill.deny?.some((a) => a.toLowerCase() === lowerAgentName)) return false + if (skill.allow?.length && !skill.allow.some((a) => a.toLowerCase() === lowerAgentName)) return false + + // Check permission ruleset + // Check if skill tool is explicitly disabled + const toolRule = PermissionNext.evaluate("skill", "*", ruleset) + if (toolRule.action === "deny") return false - const rule = PermissionNext.evaluate("skill", skill.name, ruleset) - return rule.action !== "deny" + // Check specific skill permission + const skillRule = PermissionNext.evaluate("skill", skill.name, ruleset) + return skillRule.action !== "deny" }) return c.json(filtered) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 6cc86048d12..b758c4b0122 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -330,6 +330,7 @@ export class Skill extends HeyApiClient { parameters?: { directory?: string sessionID?: string + agent?: string }, options?: Options, ) { @@ -340,6 +341,7 @@ export class Skill extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "sessionID" }, + { in: "query", key: "agent" }, ], }, ], diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 282d094374d..240d3db8286 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2119,6 +2119,7 @@ export type SkillListData = { query?: { directory?: string sessionID?: string + agent?: string } url: "/skill" } From 2991655914092d6d9c0c0ab8420f68c355fb38bd Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Sun, 4 Jan 2026 13:53:12 +0000 Subject: [PATCH 5/7] fix: enforce specificity in permission evaluation for correct skill filtering --- packages/opencode/src/permission/next.ts | 19 +++++++++++++++++-- packages/opencode/src/server/skill.ts | 19 +++---------------- packages/opencode/src/skill/skill.ts | 13 +++---------- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/packages/opencode/src/permission/next.ts b/packages/opencode/src/permission/next.ts index 6d18caefb38..423a62d6ceb 100644 --- a/packages/opencode/src/permission/next.ts +++ b/packages/opencode/src/permission/next.ts @@ -220,10 +220,25 @@ export namespace PermissionNext { export function evaluate(permission: string, pattern: string, ...rulesets: Ruleset[]): Rule { const merged = merge(...rulesets) log.info("evaluate", { permission, pattern, ruleset: merged }) - const match = merged.findLast( + const matches = merged.filter( (rule) => Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern), ) - return match ?? { action: "ask", permission, pattern: "*" } + + if (matches.length === 0) return { action: "ask", permission, pattern: "*" } + + // Sort by specificity: * (0) < glob (1) < exact (2) + // If specificity is equal, preserve original order (stable sort) via finding index? + // JS sort is stable in recent versions, but let's just rely on score. + matches.sort((a, b) => { + const getScore = (p: string) => { + if (p === "*") return 0 + if (p === pattern) return 2 + return 1 + } + return getScore(a.pattern) - getScore(b.pattern) + }) + + return matches[matches.length - 1] } const EDIT_TOOLS = ["edit", "write", "patch", "multiedit"] diff --git a/packages/opencode/src/server/skill.ts b/packages/opencode/src/server/skill.ts index 32f798f8431..030e48065da 100644 --- a/packages/opencode/src/server/skill.ts +++ b/packages/opencode/src/server/skill.ts @@ -1,12 +1,11 @@ +// Skill route - returns skills filtered by agent permissions. import { Hono } from "hono" import { describeRoute } from "hono-openapi" import { resolver, validator } from "hono-openapi" import { Skill } from "../skill/skill" import { Session } from "../session" import { Agent } from "../agent/agent" -import { Config } from "../config/config" import { PermissionNext } from "@/permission/next" -import { MessageV2 } from "../session/message-v2" import z from "zod" export const SkillRoute = new Hono().get( @@ -30,7 +29,6 @@ export const SkillRoute = new Hono().get( async (c) => { const { sessionID, agent: agentParam } = c.req.valid("query") const skills = await Skill.all() - const cfg = await Config.get() const agentName = (agentParam && agentParam !== "undefined" && agentParam !== "" ? agentParam : undefined) ?? @@ -48,19 +46,8 @@ export const SkillRoute = new Hono().get( const ruleset = PermissionNext.merge(agent.permission, session?.permission ?? []) const filtered = skills.filter((skill) => { - // Check frontmatter allow/deny (case-insensitive) - const lowerAgentName = agentName.toLowerCase() - if (skill.deny?.some((a) => a.toLowerCase() === lowerAgentName)) return false - if (skill.allow?.length && !skill.allow.some((a) => a.toLowerCase() === lowerAgentName)) return false - - // Check permission ruleset - // Check if skill tool is explicitly disabled - const toolRule = PermissionNext.evaluate("skill", "*", ruleset) - if (toolRule.action === "deny") return false - - // Check specific skill permission - const skillRule = PermissionNext.evaluate("skill", skill.name, ruleset) - return skillRule.action !== "deny" + const rule = PermissionNext.evaluate("skill", skill.name, ruleset) + return rule.action !== "deny" }) return c.json(filtered) diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index 26a7178cf95..bf90dd5870c 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -14,14 +14,6 @@ export namespace Skill { name: z.string(), description: z.string(), location: z.string(), - allow: z - .union([z.string(), z.array(z.string())]) - .optional() - .transform((x) => (typeof x === "string" ? [x] : x)), - deny: z - .union([z.string(), z.array(z.string())]) - .optional() - .transform((x) => (typeof x === "string" ? [x] : x)), }) export type Info = z.infer @@ -55,7 +47,7 @@ export namespace Skill { return } - const parsed = Info.omit({ location: true }).safeParse(md.data) + const parsed = Info.pick({ name: true, description: true }).safeParse(md.data) if (!parsed.success) return // Warn on duplicate skill names @@ -68,7 +60,8 @@ export namespace Skill { } skills[parsed.data.name] = { - ...parsed.data, + name: parsed.data.name, + description: parsed.data.description, location: match, } } From ce8142b56821391e6956e5002bb54fb1f8127322 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Sun, 4 Jan 2026 15:14:47 +0000 Subject: [PATCH 6/7] chore: sync branch to use external config symlinks --- .github | 1 + .github/ISSUE_TEMPLATE/bug-report.yml | 67 ------ .github/ISSUE_TEMPLATE/config.yml | 5 - .github/ISSUE_TEMPLATE/feature-request.yml | 20 -- .github/ISSUE_TEMPLATE/question.yml | 11 - .github/actions/setup-bun/action.yml | 22 -- .github/publish-python-sdk.yml | 71 ------ .github/workflows/deploy.yml | 29 --- .github/workflows/docs-update.yml | 72 ------ .github/workflows/duplicate-issues.yml | 63 ------ .github/workflows/generate.yml | 51 ----- .github/workflows/notify-discord.yml | 14 -- .github/workflows/opencode.yml | 32 --- .github/workflows/publish-github-action.yml | 30 --- .github/workflows/publish-vscode.yml | 37 ---- .github/workflows/publish.yml.disabled | 233 -------------------- .github/workflows/release-github-action.yml | 29 --- .github/workflows/review.yml | 83 ------- .github/workflows/stale-issues.yml | 33 --- .github/workflows/stats.yml | 35 --- .github/workflows/sync-zed-extension.yml | 35 --- .github/workflows/test.yml.disabled | 28 --- .github/workflows/triage.yml | 37 ---- .github/workflows/typecheck.yml.disabled | 19 -- .github/workflows/update-nix-hashes.yml | 102 --------- 25 files changed, 1 insertion(+), 1158 deletions(-) create mode 120000 .github delete mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml delete mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/ISSUE_TEMPLATE/feature-request.yml delete mode 100644 .github/ISSUE_TEMPLATE/question.yml delete mode 100644 .github/actions/setup-bun/action.yml delete mode 100644 .github/publish-python-sdk.yml delete mode 100644 .github/workflows/deploy.yml delete mode 100644 .github/workflows/docs-update.yml delete mode 100644 .github/workflows/duplicate-issues.yml delete mode 100644 .github/workflows/generate.yml delete mode 100644 .github/workflows/notify-discord.yml delete mode 100644 .github/workflows/opencode.yml delete mode 100644 .github/workflows/publish-github-action.yml delete mode 100644 .github/workflows/publish-vscode.yml delete mode 100644 .github/workflows/publish.yml.disabled delete mode 100644 .github/workflows/release-github-action.yml delete mode 100644 .github/workflows/review.yml delete mode 100644 .github/workflows/stale-issues.yml delete mode 100644 .github/workflows/stats.yml delete mode 100644 .github/workflows/sync-zed-extension.yml delete mode 100644 .github/workflows/test.yml.disabled delete mode 100644 .github/workflows/triage.yml delete mode 100644 .github/workflows/typecheck.yml.disabled delete mode 100644 .github/workflows/update-nix-hashes.yml diff --git a/.github b/.github new file mode 120000 index 00000000000..f338b252e4c --- /dev/null +++ b/.github @@ -0,0 +1 @@ +/home/igorw/Work/howcode-config/.github \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml deleted file mode 100644 index fe1ec8409b4..00000000000 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: Bug report -description: Report an issue that should be fixed -labels: ["bug"] -body: - - type: textarea - id: description - attributes: - label: Description - description: Describe the bug you encountered - placeholder: What happened? - validations: - required: true - - - type: input - id: plugins - attributes: - label: Plugins - description: What plugins are you using? - validations: - required: false - - - type: input - id: opencode-version - attributes: - label: OpenCode version - description: What version of OpenCode are you using? - validations: - required: false - - - type: textarea - id: reproduce - attributes: - label: Steps to reproduce - description: How can we reproduce this issue? - placeholder: | - 1. - 2. - 3. - validations: - required: false - - - type: textarea - id: screenshot-or-link - attributes: - label: Screenshot and/or share link - description: Run `/share` to get a share link, or attach a screenshot - placeholder: Paste link or drag and drop screenshot here - validations: - required: false - - - type: input - id: os - attributes: - label: Operating System - description: what OS are you using? - placeholder: e.g., macOS 26.0.1, Ubuntu 22.04, Windows 11 - validations: - required: false - - - type: input - id: terminal - attributes: - label: Terminal - description: what terminal are you using? - placeholder: e.g., iTerm2, Ghostty, Alacritty, Windows Terminal - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 459ce25d05b..00000000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,5 +0,0 @@ -blank_issues_enabled: true -contact_links: - - name: 💬 Discord Community - url: https://discord.gg/opencode - about: For quick questions or real-time discussion. Note that issues are searchable and help others with the same question. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml deleted file mode 100644 index 92e6c47570a..00000000000 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: 🚀 Feature Request -description: Suggest an idea, feature, or enhancement -labels: [discussion] -title: "[FEATURE]:" - -body: - - type: checkboxes - id: verified - attributes: - label: Feature hasn't been suggested before. - options: - - label: I have verified this feature I'm about to request hasn't been suggested before. - required: true - - - type: textarea - attributes: - label: Describe the enhancement you want to request - description: What do you want to change or add? What are the benefits of implementing this? Try to be detailed so we can understand your request better :) - validations: - required: true diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml deleted file mode 100644 index 2310bfcc86b..00000000000 --- a/.github/ISSUE_TEMPLATE/question.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: Question -description: Ask a question -labels: ["question"] -body: - - type: textarea - id: question - attributes: - label: Question - description: What's your question? - validations: - required: true diff --git a/.github/actions/setup-bun/action.yml b/.github/actions/setup-bun/action.yml deleted file mode 100644 index cba04faccef..00000000000 --- a/.github/actions/setup-bun/action.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: "Setup Bun" -description: "Setup Bun with caching and install dependencies" -runs: - using: "composite" - steps: - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version-file: package.json - - - name: Cache ~/.bun - id: cache-bun - uses: actions/cache@v4 - with: - path: ~/.bun - key: ${{ runner.os }}-bun-${{ hashFiles('package.json') }}-${{ hashFiles('bun.lockb', 'bun.lock') }} - restore-keys: | - ${{ runner.os }}-bun-${{ hashFiles('package.json') }}- - - - name: Install dependencies - run: bun install - shell: bash diff --git a/.github/publish-python-sdk.yml b/.github/publish-python-sdk.yml deleted file mode 100644 index 151ecb9944b..00000000000 --- a/.github/publish-python-sdk.yml +++ /dev/null @@ -1,71 +0,0 @@ -# -# This file is intentionally in the wrong dir, will move and add later.... -# - -# name: publish-python-sdk - -# on: -# release: -# types: [published] -# workflow_dispatch: - -# jobs: -# publish: -# runs-on: ubuntu-latest -# permissions: -# contents: read -# steps: -# - name: Checkout repository -# uses: actions/checkout@v4 - -# - name: Setup Bun -# uses: oven-sh/setup-bun@v1 -# with: -# bun-version: 1.2.21 - -# - name: Install dependencies (JS/Bun) -# run: bun install - -# - name: Install uv -# shell: bash -# run: curl -LsSf https://astral.sh/uv/install.sh | sh - -# - name: Generate Python SDK from OpenAPI (CLI) -# shell: bash -# run: | -# ~/.local/bin/uv run --project packages/sdk/python python packages/sdk/python/scripts/generate.py --source cli - -# - name: Sync Python dependencies -# shell: bash -# run: | -# ~/.local/bin/uv sync --dev --project packages/sdk/python - -# - name: Set version from release tag -# shell: bash -# run: | -# TAG="${GITHUB_REF_NAME:-}" -# if [ -z "$TAG" ]; then -# TAG="$(git describe --tags --abbrev=0 || echo 0.0.0)" -# fi -# echo "Using version: $TAG" -# VERSION="$TAG" ~/.local/bin/uv run --project packages/sdk/python python - <<'PY' -# import os, re, pathlib -# root = pathlib.Path('packages/sdk/python') -# pt = (root / 'pyproject.toml').read_text() -# version = os.environ.get('VERSION','0.0.0').lstrip('v') -# pt = re.sub(r'(?m)^(version\s*=\s*")[^"]+("\s*)$', f"\\1{version}\\2", pt) -# (root / 'pyproject.toml').write_text(pt) -# # Also update generator config override for consistency -# cfgp = root / 'openapi-python-client.yaml' -# if cfgp.exists(): -# cfg = cfgp.read_text() -# cfg = re.sub(r'(?m)^(package_version_override:\s*)\S+$', f"\\1{version}", cfg) -# cfgp.write_text(cfg) -# PY - -# - name: Build and publish to PyPI -# env: -# PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} -# shell: bash -# run: | -# ~/.local/bin/uv run --project packages/sdk/python python packages/sdk/python/scripts/publish.py diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 25466a63e06..00000000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: deploy - -on: - push: - branches: - - dev - - production - workflow_dispatch: - -concurrency: ${{ github.workflow }}-${{ github.ref }} - -jobs: - deploy: - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - uses: actions/checkout@v3 - - - uses: ./.github/actions/setup-bun - - - uses: actions/setup-node@v4 - with: - node-version: "24" - - - run: bun sst deploy --stage=${{ github.ref_name }} - env: - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - PLANETSCALE_SERVICE_TOKEN_NAME: ${{ secrets.PLANETSCALE_SERVICE_TOKEN_NAME }} - PLANETSCALE_SERVICE_TOKEN: ${{ secrets.PLANETSCALE_SERVICE_TOKEN }} - STRIPE_SECRET_KEY: ${{ github.ref_name == 'production' && secrets.STRIPE_SECRET_KEY_PROD || secrets.STRIPE_SECRET_KEY_DEV }} diff --git a/.github/workflows/docs-update.yml b/.github/workflows/docs-update.yml deleted file mode 100644 index a8dd2ae4f2b..00000000000 --- a/.github/workflows/docs-update.yml +++ /dev/null @@ -1,72 +0,0 @@ -name: Docs Update - -on: - schedule: - - cron: "0 */12 * * *" - workflow_dispatch: - -env: - LOOKBACK_HOURS: 4 - -jobs: - update-docs: - if: github.repository == 'sst/opencode' - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - id-token: write - contents: write - pull-requests: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Fetch full history to access commits - - - name: Setup Bun - uses: ./.github/actions/setup-bun - - - name: Get recent commits - id: commits - run: | - COMMITS=$(git log --since="${{ env.LOOKBACK_HOURS }} hours ago" --pretty=format:"- %h %s" 2>/dev/null || echo "") - if [ -z "$COMMITS" ]; then - echo "No commits in the last ${{ env.LOOKBACK_HOURS }} hours" - echo "has_commits=false" >> $GITHUB_OUTPUT - else - echo "has_commits=true" >> $GITHUB_OUTPUT - { - echo "list<> $GITHUB_OUTPUT - fi - - - name: Run opencode - if: steps.commits.outputs.has_commits == 'true' - uses: sst/opencode/github@latest - env: - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - with: - model: opencode/gpt-5.2 - agent: docs - prompt: | - Review the following commits from the last ${{ env.LOOKBACK_HOURS }} hours and identify any new features that may need documentation. - - - ${{ steps.commits.outputs.list }} - - - Steps: - 1. For each commit that looks like a new feature or significant change: - - Read the changed files to understand what was added - - Check if the feature is already documented in packages/web/src/content/docs/* - 2. If you find undocumented features: - - Update the relevant documentation files in packages/web/src/content/docs/* - - Follow the existing documentation style and structure - - Make sure to document the feature clearly with examples where appropriate - 3. If all new features are already documented, report that no updates are needed - 4. If you are creating a new documentation file be sure to update packages/web/astro.config.mjs too. - - Focus on user-facing features and API changes. Skip internal refactors, bug fixes, and test updates unless they affect user-facing behavior. - Don't feel the need to document every little thing. It is perfectly okay to make 0 changes at all. - Try to keep documentation only for large features or changes that already have a good spot to be documented. diff --git a/.github/workflows/duplicate-issues.yml b/.github/workflows/duplicate-issues.yml deleted file mode 100644 index dc82d297bd1..00000000000 --- a/.github/workflows/duplicate-issues.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Duplicate Issue Detection - -on: - issues: - types: [opened] - -jobs: - check-duplicates: - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - contents: read - issues: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - uses: ./.github/actions/setup-bun - - - name: Install opencode - run: curl -fsSL https://opencode.ai/install | bash - - - name: Check for duplicate issues - env: - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - OPENCODE_PERMISSION: | - { - "bash": { - "gh issue*": "allow", - "*": "deny" - }, - "webfetch": "deny" - } - run: | - opencode run -m opencode/claude-haiku-4-5 "A new issue has been created:' - - Issue number: - ${{ github.event.issue.number }} - - Lookup this issue and search through existing issues (excluding #${{ github.event.issue.number }}) in this repository to find any potential duplicates of this new issue. - Consider: - 1. Similar titles or descriptions - 2. Same error messages or symptoms - 3. Related functionality or components - 4. Similar feature requests - - If you find any potential duplicates, please comment on the new issue with: - - A brief explanation of why it might be a duplicate - - Links to the potentially duplicate issues - - A suggestion to check those issues first - - Use this format for the comment: - 'This issue might be a duplicate of existing issues. Please check: - - #[issue_number]: [brief description of similarity] - - Feel free to ignore if none of these address your specific case.' - - Additionally, if the issue mentions keybinds, keyboard shortcuts, or key bindings, please add a comment mentioning the pinned keybinds issue #4997: - 'For keybind-related issues, please also check our pinned keybinds documentation: #4997' - - If no clear duplicates are found, do not comment." diff --git a/.github/workflows/generate.yml b/.github/workflows/generate.yml deleted file mode 100644 index 29cc9895393..00000000000 --- a/.github/workflows/generate.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: generate - -on: - push: - branches: - - dev - workflow_dispatch: - -jobs: - generate: - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - contents: write - pull-requests: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} - ref: ${{ github.event.pull_request.head.ref || github.ref_name }} - - - name: Setup Bun - uses: ./.github/actions/setup-bun - - - name: Generate - run: ./script/generate.ts - - - name: Commit and push - run: | - if [ -z "$(git status --porcelain)" ]; then - echo "No changes to commit" - exit 0 - fi - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - git add -A - git commit -m "chore: generate" - git push origin HEAD:${{ github.ref_name }} --no-verify - # if ! git push origin HEAD:${{ github.event.pull_request.head.ref || github.ref_name }} --no-verify; then - # echo "" - # echo "============================================" - # echo "Failed to push generated code." - # echo "Please run locally and push:" - # echo "" - # echo " ./script/generate.ts" - # echo " git add -A && git commit -m \"chore: generate\" && git push" - # echo "" - # echo "============================================" - # exit 1 - # fi diff --git a/.github/workflows/notify-discord.yml b/.github/workflows/notify-discord.yml deleted file mode 100644 index 62577ecf00e..00000000000 --- a/.github/workflows/notify-discord.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: discord - -on: - release: - types: [released] # fires when a draft release is published - -jobs: - notify: - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - name: Send nicely-formatted embed to Discord - uses: SethCohen/github-releases-to-discord@v1 - with: - webhook_url: ${{ secrets.DISCORD_WEBHOOK }} diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml deleted file mode 100644 index a8a02185e43..00000000000 --- a/.github/workflows/opencode.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: opencode - -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - -jobs: - opencode: - if: | - contains(github.event.comment.body, ' /oc') || - startsWith(github.event.comment.body, '/oc') || - contains(github.event.comment.body, ' /opencode') || - startsWith(github.event.comment.body, '/opencode') - runs-on: ubuntu-latest - permissions: - id-token: write - contents: write - pull-requests: write - issues: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Run opencode - uses: anomalyco/opencode/github@latest - env: - ZHIPU_API_KEY: ${{ secrets.ZHIPU_API_KEY }} - OPENCODE_PERMISSION: '{"bash": "deny"}' - with: - model: zai-coding-plan/glm-4.7 diff --git a/.github/workflows/publish-github-action.yml b/.github/workflows/publish-github-action.yml deleted file mode 100644 index d2789373a34..00000000000 --- a/.github/workflows/publish-github-action.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: publish-github-action - -on: - workflow_dispatch: - push: - tags: - - "github-v*.*.*" - - "!github-v1" - -concurrency: ${{ github.workflow }}-${{ github.ref }} - -permissions: - contents: write - -jobs: - publish: - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - run: git fetch --force --tags - - - name: Publish - run: | - git config --global user.email "opencode@sst.dev" - git config --global user.name "opencode" - ./script/publish - working-directory: ./github diff --git a/.github/workflows/publish-vscode.yml b/.github/workflows/publish-vscode.yml deleted file mode 100644 index f49a1057807..00000000000 --- a/.github/workflows/publish-vscode.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: publish-vscode - -on: - workflow_dispatch: - push: - tags: - - "vscode-v*.*.*" - -concurrency: ${{ github.workflow }}-${{ github.ref }} - -permissions: - contents: write - -jobs: - publish: - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - uses: ./.github/actions/setup-bun - - - run: git fetch --force --tags - - run: bun install -g @vscode/vsce - - - name: Install extension dependencies - run: bun install - working-directory: ./sdks/vscode - - - name: Publish - run: | - ./script/publish - working-directory: ./sdks/vscode - env: - VSCE_PAT: ${{ secrets.VSCE_PAT }} - OPENVSX_TOKEN: ${{ secrets.OPENVSX_TOKEN }} diff --git a/.github/workflows/publish.yml.disabled b/.github/workflows/publish.yml.disabled deleted file mode 100644 index 5720996170e..00000000000 --- a/.github/workflows/publish.yml.disabled +++ /dev/null @@ -1,233 +0,0 @@ -name: publish -run-name: "${{ format('release {0}', inputs.bump) }}" - -on: - push: - branches: - - dev - - snapshot-* - workflow_dispatch: - inputs: - bump: - description: "Bump major, minor, or patch" - required: false - type: choice - options: - - major - - minor - - patch - version: - description: "Override version (optional)" - required: false - type: string - -concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version || inputs.bump }} - -permissions: - id-token: write - contents: write - packages: write - -jobs: - publish: - runs-on: blacksmith-4vcpu-ubuntu-2404 - if: github.repository == 'anomalyco/opencode' - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - run: git fetch --force --tags - - - uses: ./.github/actions/setup-bun - - - name: Install OpenCode - if: inputs.bump || inputs.version - run: bun i -g opencode-ai@1.0.169 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - uses: actions/setup-node@v4 - with: - node-version: "24" - registry-url: "https://registry.npmjs.org" - - - name: Setup Git Identity - run: | - git config --global user.email "opencode@sst.dev" - git config --global user.name "opencode" - git remote set-url origin https://x-access-token:${{ secrets.SST_GITHUB_TOKEN }}@github.com/${{ github.repository }} - - - name: Publish - id: publish - run: ./script/publish-start.ts - env: - OPENCODE_BUMP: ${{ inputs.bump }} - OPENCODE_VERSION: ${{ inputs.version }} - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - AUR_KEY: ${{ secrets.AUR_KEY }} - GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }} - NPM_CONFIG_PROVENANCE: false - - - uses: actions/upload-artifact@v4 - with: - name: opencode-cli - path: packages/opencode/dist - - outputs: - release: ${{ steps.publish.outputs.release }} - tag: ${{ steps.publish.outputs.tag }} - version: ${{ steps.publish.outputs.version }} - - publish-tauri: - needs: publish - continue-on-error: true - strategy: - fail-fast: false - matrix: - settings: - - host: macos-latest - target: x86_64-apple-darwin - - host: macos-latest - target: aarch64-apple-darwin - - host: blacksmith-4vcpu-windows-2025 - target: x86_64-pc-windows-msvc - - host: blacksmith-4vcpu-ubuntu-2404 - target: x86_64-unknown-linux-gnu - - host: blacksmith-4vcpu-ubuntu-2404-arm - target: aarch64-unknown-linux-gnu - runs-on: ${{ matrix.settings.host }} - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - ref: ${{ needs.publish.outputs.tag }} - - - uses: apple-actions/import-codesign-certs@v2 - if: ${{ runner.os == 'macOS' }} - with: - keychain: build - p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }} - p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - - - name: Verify Certificate - if: ${{ runner.os == 'macOS' }} - run: | - CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application") - CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}') - echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV - echo "Certificate imported." - - - name: Setup Apple API Key - if: ${{ runner.os == 'macOS' }} - run: | - echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8 - - - run: git fetch --force --tags - - - uses: ./.github/actions/setup-bun - - - name: install dependencies (ubuntu only) - if: contains(matrix.settings.host, 'ubuntu') - run: | - sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf - - - name: install Rust stable - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.settings.target }} - - - uses: Swatinem/rust-cache@v2 - with: - workspaces: packages/desktop/src-tauri - shared-key: ${{ matrix.settings.target }} - - - name: Prepare - run: | - cd packages/desktop - bun ./scripts/prepare.ts - env: - OPENCODE_VERSION: ${{ needs.publish.outputs.version }} - NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }} - GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }} - AUR_KEY: ${{ secrets.AUR_KEY }} - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - RUST_TARGET: ${{ matrix.settings.target }} - GH_TOKEN: ${{ github.token }} - GITHUB_RUN_ID: ${{ github.run_id }} - - # Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released - - name: Install tauri-cli from portable appimage branch - if: contains(matrix.settings.host, 'ubuntu') - run: | - cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage --force - echo "Installed tauri-cli version:" - cargo tauri --version - - - name: Build and upload artifacts - timeout-minutes: 20 - uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: true - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }} - APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} - APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} - APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8 - with: - projectPath: packages/desktop - uploadWorkflowArtifacts: true - tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }} - args: --target ${{ matrix.settings.target }} --config ./src-tauri/tauri.prod.conf.json --verbose - updaterJsonPreferNsis: true - releaseId: ${{ needs.publish.outputs.release }} - tagName: ${{ needs.publish.outputs.tag }} - releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext] - releaseDraft: true - - publish-release: - needs: - - publish - - publish-tauri - if: needs.publish.outputs.tag - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - ref: ${{ needs.publish.outputs.tag }} - - - uses: ./.github/actions/setup-bun - - - name: Setup SSH for AUR - run: | - sudo apt-get update - sudo apt-get install -y pacman-package-manager - mkdir -p ~/.ssh - echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa - git config --global user.email "opencode@sst.dev" - git config --global user.name "opencode" - ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true - - - run: ./script/publish-complete.ts - env: - OPENCODE_VERSION: ${{ needs.publish.outputs.version }} - AUR_KEY: ${{ secrets.AUR_KEY }} - GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }} diff --git a/.github/workflows/release-github-action.yml b/.github/workflows/release-github-action.yml deleted file mode 100644 index 3f5caa55c8d..00000000000 --- a/.github/workflows/release-github-action.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: release-github-action - -on: - push: - branches: - - dev - paths: - - "github/**" - -concurrency: ${{ github.workflow }}-${{ github.ref }} - -permissions: - contents: write - -jobs: - release: - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - run: git fetch --force --tags - - - name: Release - run: | - git config --global user.email "opencode@sst.dev" - git config --global user.name "opencode" - ./github/script/release diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml deleted file mode 100644 index 44bfeb33661..00000000000 --- a/.github/workflows/review.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: Guidelines Check - -on: - issue_comment: - types: [created] - -jobs: - check-guidelines: - if: | - github.event.issue.pull_request && - startsWith(github.event.comment.body, '/review') && - contains(fromJson('["OWNER","MEMBER"]'), github.event.comment.author_association) - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - contents: read - pull-requests: write - steps: - - name: Get PR number - id: pr-number - run: | - if [ "${{ github.event_name }}" = "pull_request_target" ]; then - echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT - else - echo "number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT - fi - - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - uses: ./.github/actions/setup-bun - - - name: Install opencode - run: curl -fsSL https://opencode.ai/install | bash - - - name: Get PR details - id: pr-details - run: | - gh api /repos/${{ github.repository }}/pulls/${{ steps.pr-number.outputs.number }} > pr_data.json - echo "title=$(jq -r .title pr_data.json)" >> $GITHUB_OUTPUT - echo "sha=$(jq -r .head.sha pr_data.json)" >> $GITHUB_OUTPUT - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Check PR guidelines compliance - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - OPENCODE_PERMISSION: '{ "bash": { "gh*": "allow", "gh pr review*": "deny", "*": "deny" } }' - PR_TITLE: ${{ steps.pr-details.outputs.title }} - run: | - PR_BODY=$(jq -r .body pr_data.json) - opencode run -m anthropic/claude-opus-4-5 "A new pull request has been created: '${PR_TITLE}' - - - ${{ steps.pr-number.outputs.number }} - - - - $PR_BODY - - - Please check all the code changes in this pull request against the style guide, also look for any bugs if they exist. Diffs are important but make sure you read the entire file to get proper context. Make it clear the suggestions are merely suggestions and the human can decide what to do - - When critiquing code against the style guide, be sure that the code is ACTUALLY in violation, don't complain about else statements if they already use early returns there. You may complain about excessive nesting though, regardless of else statement usage. - When critiquing code style don't be a zealot, we don't like "let" statements but sometimes they are the simplest option, if someone does a bunch of nesting with let, they should consider using iife (see packages/opencode/src/util.iife.ts) - - Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block. - If you are writing suggested fixes, BE SURE THAT the change you are recommending is actually valid typescript, often I have seen missing closing "}" or other syntax errors. - Generally, write a comment instead of writing suggested change if you can help it. - - Command MUST be like this. - \`\`\` - gh api \ - --method POST \ - -H \"Accept: application/vnd.github+json\" \ - -H \"X-GitHub-Api-Version: 2022-11-28\" \ - /repos/${{ github.repository }}/pulls/${{ steps.pr-number.outputs.number }}/comments \ - -f 'body=[summary of issue]' -f 'commit_id=${{ steps.pr-details.outputs.sha }}' -f 'path=[path-to-file]' -F \"line=[line]\" -f 'side=RIGHT' - \`\`\` - - Only create comments for actual violations. If the code follows all guidelines, comment on the issue using gh cli: 'lgtm' AND NOTHING ELSE!!!!." diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml deleted file mode 100644 index b5378d7d527..00000000000 --- a/.github/workflows/stale-issues.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: "Auto-close stale issues" - -on: - schedule: - - cron: "30 1 * * *" # Daily at 1:30 AM - workflow_dispatch: - -env: - DAYS_BEFORE_STALE: 90 - DAYS_BEFORE_CLOSE: 7 - -jobs: - stale: - runs-on: ubuntu-latest - permissions: - issues: write - steps: - - uses: actions/stale@v10 - with: - days-before-stale: ${{ env.DAYS_BEFORE_STALE }} - days-before-close: ${{ env.DAYS_BEFORE_CLOSE }} - stale-issue-label: "stale" - close-issue-message: | - [automated] Closing due to ${{ env.DAYS_BEFORE_STALE }}+ days of inactivity. - - Feel free to reopen if you still need this! - stale-issue-message: | - [automated] This issue has had no activity for ${{ env.DAYS_BEFORE_STALE }} days. - - It will be closed in ${{ env.DAYS_BEFORE_CLOSE }} days if there's no new activity. - remove-stale-when-updated: true - exempt-issue-labels: "pinned,security,feature-request,on-hold" - start-date: "2025-12-27" diff --git a/.github/workflows/stats.yml b/.github/workflows/stats.yml deleted file mode 100644 index 57e93642b27..00000000000 --- a/.github/workflows/stats.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: stats - -on: - schedule: - - cron: "0 12 * * *" # Run daily at 12:00 UTC - workflow_dispatch: # Allow manual trigger - -concurrency: ${{ github.workflow }}-${{ github.ref }} - -jobs: - stats: - if: github.repository == 'sst/opencode' - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - contents: write - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Bun - uses: ./.github/actions/setup-bun - - - name: Run stats script - run: bun script/stats.ts - - - name: Commit stats - run: | - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - git add STATS.md - git diff --staged --quiet || git commit -m "ignore: update download stats $(date -I)" - git push - env: - POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }} diff --git a/.github/workflows/sync-zed-extension.yml b/.github/workflows/sync-zed-extension.yml deleted file mode 100644 index f14487cde97..00000000000 --- a/.github/workflows/sync-zed-extension.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: "sync-zed-extension" - -on: - workflow_dispatch: - release: - types: [published] - -jobs: - zed: - name: Release Zed Extension - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: ./.github/actions/setup-bun - - - name: Get version tag - id: get_tag - run: | - if [ "${{ github.event_name }}" = "release" ]; then - TAG="${{ github.event.release.tag_name }}" - else - TAG=$(git tag --list 'v[0-9]*.*' --sort=-version:refname | head -n 1) - fi - echo "tag=${TAG}" >> $GITHUB_OUTPUT - echo "Using tag: ${TAG}" - - - name: Sync Zed extension - run: | - ./script/sync-zed.ts ${{ steps.get_tag.outputs.tag }} - env: - ZED_EXTENSIONS_PAT: ${{ secrets.ZED_EXTENSIONS_PAT }} - ZED_PR_PAT: ${{ secrets.ZED_PR_PAT }} diff --git a/.github/workflows/test.yml.disabled b/.github/workflows/test.yml.disabled deleted file mode 100644 index c39710bee8f..00000000000 --- a/.github/workflows/test.yml.disabled +++ /dev/null @@ -1,28 +0,0 @@ -name: test - -on: - push: - branches: - - dev - pull_request: - workflow_dispatch: -jobs: - test: - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup Bun - uses: ./.github/actions/setup-bun - - - name: run - run: | - git config --global user.email "bot@opencode.ai" - git config --global user.name "opencode" - bun turbo typecheck - bun turbo test - env: - CI: true diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml deleted file mode 100644 index 6e150957291..00000000000 --- a/.github/workflows/triage.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Issue Triage - -on: - issues: - types: [opened] - -jobs: - triage: - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - contents: read - issues: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Setup Bun - uses: ./.github/actions/setup-bun - - - name: Install opencode - run: curl -fsSL https://opencode.ai/install | bash - - - name: Triage issue - env: - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ISSUE_NUMBER: ${{ github.event.issue.number }} - ISSUE_TITLE: ${{ github.event.issue.title }} - ISSUE_BODY: ${{ github.event.issue.body }} - run: | - opencode run --agent triage "The following issue was just opened, triage it: - - Title: $ISSUE_TITLE - - $ISSUE_BODY" diff --git a/.github/workflows/typecheck.yml.disabled b/.github/workflows/typecheck.yml.disabled deleted file mode 100644 index 011e23f5f6f..00000000000 --- a/.github/workflows/typecheck.yml.disabled +++ /dev/null @@ -1,19 +0,0 @@ -name: typecheck - -on: - pull_request: - branches: [dev] - workflow_dispatch: - -jobs: - typecheck: - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Bun - uses: ./.github/actions/setup-bun - - - name: Run typecheck - run: bun typecheck diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml deleted file mode 100644 index d2c60b08f01..00000000000 --- a/.github/workflows/update-nix-hashes.yml +++ /dev/null @@ -1,102 +0,0 @@ -name: Update Nix Hashes - -permissions: - contents: write - -on: - workflow_dispatch: - push: - paths: - - "bun.lock" - - "package.json" - - "packages/*/package.json" - pull_request: - paths: - - "bun.lock" - - "package.json" - - "packages/*/package.json" - -jobs: - update: - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - runs-on: blacksmith-4vcpu-ubuntu-2404 - env: - SYSTEM: x86_64-linux - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - fetch-depth: 0 - ref: ${{ github.head_ref || github.ref_name }} - repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} - - - name: Setup Nix - uses: DeterminateSystems/nix-installer-action@v20 - - - name: Configure git - run: | - git config --global user.email "action@github.com" - git config --global user.name "Github Action" - - - name: Update flake.lock - run: | - set -euo pipefail - echo "📦 Updating flake.lock..." - nix flake update - echo "✅ flake.lock updated successfully" - - - name: Update node_modules hash - run: | - set -euo pipefail - echo "🔄 Updating node_modules hash..." - nix/scripts/update-hashes.sh - echo "✅ node_modules hash updated successfully" - - - name: Commit hash changes - env: - TARGET_BRANCH: ${{ github.head_ref || github.ref_name }} - run: | - set -euo pipefail - - echo "🔍 Checking for changes in tracked Nix files..." - - summarize() { - local status="$1" - { - echo "### Nix Hash Update" - echo "" - echo "- ref: ${GITHUB_REF_NAME}" - echo "- status: ${status}" - } >> "$GITHUB_STEP_SUMMARY" - if [ -n "${GITHUB_SERVER_URL:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_RUN_ID:-}" ]; then - echo "- run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" >> "$GITHUB_STEP_SUMMARY" - fi - echo "" >> "$GITHUB_STEP_SUMMARY" - } - - FILES=(flake.lock flake.nix nix/node-modules.nix nix/hashes.json) - STATUS="$(git status --short -- "${FILES[@]}" || true)" - if [ -z "$STATUS" ]; then - echo "✅ No changes detected. Hashes are already up to date." - summarize "no changes" - exit 0 - fi - - echo "📝 Changes detected:" - echo "$STATUS" - echo "🔗 Staging files..." - git add "${FILES[@]}" - echo "💾 Committing changes..." - git commit -m "Update Nix flake.lock and hashes" - echo "✅ Changes committed" - - BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}" - echo "🌳 Pulling latest from branch: $BRANCH" - git pull --rebase origin "$BRANCH" - echo "🚀 Pushing changes to branch: $BRANCH" - git push origin HEAD:"$BRANCH" - echo "✅ Changes pushed successfully" - - summarize "committed $(git rev-parse --short HEAD)" From 2d9818e61d01f06c75ec2e9bb166ed54fcbff305 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Sun, 4 Jan 2026 15:18:49 +0000 Subject: [PATCH 7/7] refactor: address PR #17 review comments Remove redundant loadSkills call and simplify agentName resolution logic. --- packages/app/src/context/global-sync.tsx | 1 - packages/opencode/src/server/skill.ts | 19 ++++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 3e17cc92577..6363ab169d9 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -151,7 +151,6 @@ function createGlobalSync() { function setSelectedAgent(directory: string, agent: string) { setGlobalStore("selected_agent", directory, agent) - loadSkills(directory, undefined, agent) } async function bootstrapInstance(directory: string) { diff --git a/packages/opencode/src/server/skill.ts b/packages/opencode/src/server/skill.ts index 030e48065da..4cc19469186 100644 --- a/packages/opencode/src/server/skill.ts +++ b/packages/opencode/src/server/skill.ts @@ -30,14 +30,19 @@ export const SkillRoute = new Hono().get( const { sessionID, agent: agentParam } = c.req.valid("query") const skills = await Skill.all() - const agentName = - (agentParam && agentParam !== "undefined" && agentParam !== "" ? agentParam : undefined) ?? - (await (async () => { - if (!sessionID) return undefined + const agentName = await (async () => { + if (agentParam && agentParam !== "undefined" && agentParam !== "") { + return agentParam + } + if (sessionID) { const messages = await Session.messages({ sessionID, limit: 10 }) - return messages.findLast((m) => m.info.role === "user")?.info.agent - })()) ?? - (await Agent.defaultAgent()) + const agentFromSession = messages.findLast((m) => m.info.role === "user")?.info.agent + if (agentFromSession) { + return agentFromSession + } + } + return Agent.defaultAgent() + })() const agent = await Agent.get(agentName) if (!agent) return c.json(skills)