From 5b784871f0befb450f0f80848e397e3f9e36b060 Mon Sep 17 00:00:00 2001 From: Dax Date: Sat, 31 Jan 2026 20:52:54 -0500 Subject: [PATCH 1/3] feat: add skill dialog for selecting and inserting skills (#11547) --- .../cli/cmd/tui/component/dialog-skill.tsx | 34 +++++++++++++++++++ .../cmd/tui/component/prompt/autocomplete.tsx | 3 +- .../cli/cmd/tui/component/prompt/index.tsx | 23 +++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 packages/opencode/src/cli/cmd/tui/component/dialog-skill.tsx diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-skill.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-skill.tsx new file mode 100644 index 0000000000..1ca109f232 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-skill.tsx @@ -0,0 +1,34 @@ +import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select" +import { createResource, createMemo } from "solid-js" +import { useDialog } from "@tui/ui/dialog" +import { useSDK } from "@tui/context/sdk" + +export type DialogSkillProps = { + onSelect: (skill: string) => void +} + +export function DialogSkill(props: DialogSkillProps) { + const dialog = useDialog() + const sdk = useSDK() + + const [skills] = createResource(async () => { + const result = await sdk.client.app.skills() + return result.data ?? [] + }) + + const options = createMemo[]>(() => { + const list = skills() ?? [] + return list.map((skill) => ({ + title: skill.name, + description: skill.description, + value: skill.name, + category: "Skills", + onSelect: () => { + props.onSelect(skill.name) + dialog.clear() + }, + })) + }) + + return +} diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index bd000e2ab0..5f66dc822a 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -345,7 +345,8 @@ export function Autocomplete(props: { const results: AutocompleteOption[] = [...command.slashes()] for (const serverCommand of sync.data.command) { - const label = serverCommand.source === "mcp" ? ":mcp" : serverCommand.source === "skill" ? ":skill" : "" + if (serverCommand.source === "skill") continue + const label = serverCommand.source === "mcp" ? ":mcp" : "" results.push({ display: "/" + serverCommand.name + label, description: serverCommand.description, diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index caa1303229..8576dd5763 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -31,6 +31,7 @@ import { DialogAlert } from "../../ui/dialog-alert" import { useToast } from "../../ui/toast" import { useKV } from "../../context/kv" import { useTextareaKeybindings } from "../textarea-keybindings" +import { DialogSkill } from "../dialog-skill" export type PromptProps = { sessionID?: string @@ -315,6 +316,28 @@ export function Prompt(props: PromptProps) { input.cursorOffset = Bun.stringWidth(content) }, }, + { + title: "Skills", + value: "prompt.skills", + category: "Prompt", + slash: { + name: "skills", + }, + onSelect: () => { + dialog.replace(() => ( + { + input.setText(`/${skill} `) + setStore("prompt", { + input: `/${skill} `, + parts: [], + }) + input.gotoBufferEnd() + }} + /> + )) + }, + }, ] }) From d4c90b2dfb89385461abf0d51430d1293e6de6a8 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 31 Jan 2026 21:01:51 -0600 Subject: [PATCH 2/3] fix: issue where you couldn't @ folders/files that started with a "." (#11553) --- packages/opencode/src/file/ripgrep.ts | 2 +- packages/opencode/test/file/ripgrep.test.ts | 39 +++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 packages/opencode/test/file/ripgrep.test.ts diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index dd94cc6097..463a9fb362 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -215,7 +215,7 @@ export namespace Ripgrep { const args = [await filepath(), "--files", "--glob=!.git/*"] if (input.follow) args.push("--follow") - if (input.hidden) args.push("--hidden") + if (input.hidden !== false) args.push("--hidden") if (input.maxDepth !== undefined) args.push(`--max-depth=${input.maxDepth}`) if (input.glob) { for (const g of input.glob) { diff --git a/packages/opencode/test/file/ripgrep.test.ts b/packages/opencode/test/file/ripgrep.test.ts new file mode 100644 index 0000000000..ac46f1131b --- /dev/null +++ b/packages/opencode/test/file/ripgrep.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, test } from "bun:test" +import fs from "fs/promises" +import path from "path" +import { tmpdir } from "../fixture/fixture" +import { Ripgrep } from "../../src/file/ripgrep" + +describe("file.ripgrep", () => { + test("defaults to include hidden", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "visible.txt"), "hello") + await fs.mkdir(path.join(dir, ".opencode"), { recursive: true }) + await Bun.write(path.join(dir, ".opencode", "thing.json"), "{}") + }, + }) + + const files = await Array.fromAsync(Ripgrep.files({ cwd: tmp.path })) + const hasVisible = files.includes("visible.txt") + const hasHidden = files.includes(path.join(".opencode", "thing.json")) + expect(hasVisible).toBe(true) + expect(hasHidden).toBe(true) + }) + + test("hidden false excludes hidden", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "visible.txt"), "hello") + await fs.mkdir(path.join(dir, ".opencode"), { recursive: true }) + await Bun.write(path.join(dir, ".opencode", "thing.json"), "{}") + }, + }) + + const files = await Array.fromAsync(Ripgrep.files({ cwd: tmp.path, hidden: false })) + const hasVisible = files.includes("visible.txt") + const hasHidden = files.includes(path.join(".opencode", "thing.json")) + expect(hasVisible).toBe(true) + expect(hasHidden).toBe(false) + }) +}) From 9e45313b0aa38fa8b157c97077964e9db8bb84e3 Mon Sep 17 00:00:00 2001 From: Goni Zahavy Date: Sun, 1 Feb 2026 05:16:34 +0200 Subject: [PATCH 3/3] ci: fixed stale pr workflow (#11310) --- .github/workflows/close-stale-prs.yml | 94 ++++++++++++++++++++++----- 1 file changed, 76 insertions(+), 18 deletions(-) diff --git a/.github/workflows/close-stale-prs.yml b/.github/workflows/close-stale-prs.yml index cb5c45063f..e1ff4241c9 100644 --- a/.github/workflows/close-stale-prs.yml +++ b/.github/workflows/close-stale-prs.yml @@ -28,40 +28,98 @@ jobs: const cutoff = new Date(Date.now() - DAYS_INACTIVE * 24 * 60 * 60 * 1000) const { owner, repo } = context.repo const dryRun = context.payload.inputs?.dryRun === "true" - const stalePrs = [] core.info(`Dry run mode: ${dryRun}`) + core.info(`Cutoff date: ${cutoff.toISOString()}`) - const prs = await github.paginate(github.rest.pulls.list, { - owner, - repo, - state: "open", - per_page: 100, - sort: "updated", - direction: "asc", - }) + const query = ` + query($owner: String!, $repo: String!, $cursor: String) { + repository(owner: $owner, name: $repo) { + pullRequests(first: 100, states: OPEN, after: $cursor) { + pageInfo { + hasNextPage + endCursor + } + nodes { + number + title + author { + login + } + createdAt + commits(last: 1) { + nodes { + commit { + committedDate + } + } + } + comments(last: 1) { + nodes { + createdAt + } + } + reviews(last: 1) { + nodes { + createdAt + } + } + } + } + } + } + ` - for (const pr of prs) { - const lastUpdated = new Date(pr.updated_at) - if (lastUpdated > cutoff) { - core.info(`PR ${pr.number} is fresh`) - continue + const allPrs = [] + let cursor = null + let hasNextPage = true + + while (hasNextPage) { + const result = await github.graphql(query, { + owner, + repo, + cursor, + }) + + allPrs.push(...result.repository.pullRequests.nodes) + hasNextPage = result.repository.pullRequests.pageInfo.hasNextPage + cursor = result.repository.pullRequests.pageInfo.endCursor + } + + core.info(`Found ${allPrs.length} open pull requests`) + + const stalePrs = allPrs.filter((pr) => { + const dates = [ + new Date(pr.createdAt), + pr.commits.nodes[0] ? new Date(pr.commits.nodes[0].commit.committedDate) : null, + pr.comments.nodes[0] ? new Date(pr.comments.nodes[0].createdAt) : null, + pr.reviews.nodes[0] ? new Date(pr.reviews.nodes[0].createdAt) : null, + ].filter((d) => d !== null) + + const lastActivity = dates.sort((a, b) => b.getTime() - a.getTime())[0] + + if (!lastActivity || lastActivity > cutoff) { + core.info(`PR #${pr.number} is fresh (last activity: ${lastActivity?.toISOString() || "unknown"})`) + return false } - stalePrs.push(pr) - } + core.info(`PR #${pr.number} is STALE (last activity: ${lastActivity.toISOString()})`) + return true + }) if (!stalePrs.length) { core.info("No stale pull requests found.") return } + core.info(`Found ${stalePrs.length} stale pull requests`) + for (const pr of stalePrs) { const issue_number = pr.number const closeComment = `Closing this pull request because it has had no updates for more than ${DAYS_INACTIVE} days. If you plan to continue working on it, feel free to reopen or open a new PR.` if (dryRun) { - core.info(`[dry-run] Would close PR #${issue_number} from ${pr.user.login}`) + core.info(`[dry-run] Would close PR #${issue_number} from ${pr.author.login}: ${pr.title}`) continue } @@ -79,5 +137,5 @@ jobs: state: "closed", }) - core.info(`Closed PR #${issue_number} from ${pr.user.login}`) + core.info(`Closed PR #${issue_number} from ${pr.author.login}: ${pr.title}`) }