Skip to content

feat(tui): install skills via typed query in the /skills list#1005

Open
sahrizvi wants to merge 2 commits into
mainfrom
fix/skills-install-from-typed-url
Open

feat(tui): install skills via typed query in the /skills list#1005
sahrizvi wants to merge 2 commits into
mainfrom
fix/skills-install-from-typed-url

Conversation

@sahrizvi

@sahrizvi sahrizvi commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Issue for this PR

Closes #

No linked issue — internal quality fix on the fork's inline /skills dialog.

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Gives the /skills list a discoverable Enter-driven install path.

When the filter text matches an installable shape (github URL, clean owner/repo, or an absolute path), a synthetic Install <query> row is prepended at the top of the list. Enter opens the install dialog with the typed text prefilled. Symmetric wiring on the create dialog.

Why: the pre-existing ctrl+i shortcut can't be relied on — its wire byte (0x09) collides with Tab on default terminals, so users trying to install from a URL/repo/path they typed into the search box saw the list filter to zero results and had no obvious next action.

How: backing the "when to offer the row" check with the same classifier the installer uses (classifyInstallSource) kills a class of bugs where the list would advertise an install of things the installer would then reject — e.g. skill-search queries like dbt/snowflake or owner/repo/subpath.

Files:

  • packages/opencode/src/plugin/tui/altimate/skill-ops.tsx
    • New classifyInstallSource(source) — recognises github-url, owner-repo, absolute-path; returns null for short strings, sub-paths, relative paths, and ~-paths.
    • New normalizeInstallSource(source) — shared trim/strip helper (also used by installSkillDirect when building the clone URL, so owner/repo.git can't produce owner/repo.git.git).
    • Module-scope INSTALL_ACTION_VALUE sentinel — namespaced so it can't collide with real skill names.
    • DialogSkillList — synthetic Install row via spread (pure memo); onSelect routes sentinel to showInstall; onMove filters sentinel; showActions bails on sentinel/lookup-miss.
    • options memo split into baseOptions (deps on skills() only) + composed options (deps on filter()) — avoids re-running detectToolReferences regex parse per skill on every keystroke.
  • packages/opencode/test/altimate/skill-install-classifier.test.ts (new) — 14 unit tests pinning classifier + normalizer edge cases (owner/repo, http(s), POSIX absolute, Windows drive-letter, short-string rejection, multi-slash rejection, ~/relative rejection, .git / trailing-dot / whitespace stripping).
  • packages/opencode/test/upstream/fork-feature-guards.test.ts — slimmed the merge-drop guard to a minimal presence check (classifier symbol, sentinel, sentinel-route, prefill plumbing on both sub-dialogs).

How did you verify your code works?

Local validation (all green):

  • bun run typecheck — clean across the whole workspace
  • bun test test/altimate/skill-install-classifier.test.ts — 14 pass / 0 fail / 54 expects
  • bun test test/upstream/fork-feature-guards.test.ts — 18 pass / 0 fail / 61 expects
  • bun test test/session — 731 pass / 0 fail / 1636 expects

Manual smoke plan on a built binary:

  • /skills → type https://github.com/anthropics/skills.git → Enter opens install dialog with URL prefilled.
  • /skills → type owner/repo → Enter opens install dialog with owner/repo prefilled → clone URL is https://github.com/owner/repo.git (single .git, no double-suffix).
  • /skills → type owner/repo.git → same clone URL (double-suffix bug regression check).
  • /skills → type dbt-develop (real search term) → no Install row surfaces; list filters skills normally.
  • Highlight the synthetic Install row → ctrl+a does nothing (sentinel bail-out).

Screenshots / recordings

Terminal-only UI change — no screenshot supplied. Behaviour is fully covered by the classifier unit tests and the fork-feature-guards presence checks.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Follow-ups intentionally deferred (from prior review)

  • Conditional placeholder text (m6) — the Search skills, or type a repo/URL… copy shows even when the filter isn't installable. Low priority.
  • Prefill cursor-at-end (m7) — DialogPrompt's cursor lands at end-of-text; fine for appending /path, mildly awkward for editing a pasted URL.

Review response

Addressed both findings from OpenCodeReview (Gemini) in commit b8e7e64c3:

  • MEDIUM Bug (skill-ops.tsx:177-180) — owner/repo.git produced owner/repo.git.git. Extracted normalizeInstallSource and use it in both classifier and URL builder. Regression covered by new normalizer test suite.
  • MEDIUM Perf (skill-ops.tsx:592-596) — filter() reactivity made detectToolReferences re-run per keystroke. Split into baseOptions (skills-only) + composed options (filter-aware).

Summary by CodeRabbit

  • New Features

    • Added support for installing skills from GitHub URLs, repository shorthand, and absolute filesystem paths.
    • Added an “Install ” option when the search text matches a recognized source.
    • Prefill create and install dialogs with the current search text.
    • Improved handling of filtered skill selections and unavailable actions.
  • Tests

    • Added coverage for install-source recognition, normalization, and the new install option.

The `/skills` list didn't offer a discoverable way to install from a
GitHub repo / URL / absolute path — the only entry point was `ctrl+i`,
whose wire byte (0x09) collides with Tab on default terminals, so many
users couldn't trigger it.

Surface a synthetic "Install <query>" row at the top of the list when the
filter matches an installable shape, so pressing Enter opens the install
dialog prefilled with the typed text. Backed by a shared
`classifyInstallSource` classifier used by both the list preview and
`installSkillDirect` — an earlier `q.includes("/")` check drifted from
the installer and offered the row for skill-search queries like
`dbt/snowflake` that the installer then rejected as "Path not found".

- Add module-scope `classifyInstallSource` (recognises github URLs, clean
  `owner/repo` shorthand, POSIX absolute paths, Windows drive-letter
  paths — rejects short strings, sub-paths, `~`, relatives).
- Route the sentinel through `onSelect` to `showInstall`, forward the
  captured filter text as `initialValue` on `DialogSkillInstall` and
  `DialogSkillCreate`.
- Filter the sentinel out of `onMove` so highlighting the synthetic row
  doesn't set `currentSkill` to the sentinel string (would trip
  `ctrl+a`'s action picker into a degenerate lookup miss).
- Harden `showActions` to bail on the sentinel or a lookup miss as
  belt-and-braces.
- Build options via a spread instead of `Array.prototype.unshift` so the
  memo stays pure (Solid dev-mode double-eval would otherwise
  double-prepend).
- Slim the fork-feature-guards test to a minimal presence check
  (sentinel, classifier symbol, sentinel-route in `onSelect`, prefill
  plumbing) — behavioural coverage of the classifier moves to a new
  `test/altimate/skill-install-classifier.test.ts` unit suite.

Verified: `bun run typecheck` clean; new classifier suite 10/10;
fork-feature-guards 18/18; `test/session` 731/731.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds shared install-source classification, synthetic install actions in the skill list, filter-prefilled create/install dialogs, and guards preventing action pickers from handling synthetic rows. Tests cover source normalization, accepted path and repository forms, and UI wiring.

Changes

Skill installation flow

Layer / File(s) Summary
Install source classification and routing
packages/opencode/src/plugin/tui/altimate/skill-ops.tsx
Adds InstallSourceKind, normalizeInstallSource(), and classifyInstallSource(), then uses the classifier to select Git-based or absolute-path installation.
Filtered install row and dialog prefill
packages/opencode/src/plugin/tui/altimate/skill-ops.tsx
Tracks the list filter, adds a synthetic install option for recognized sources, routes selection to installation, prevents synthetic rows from reaching actions, and prefills create/install prompts.
Classifier and wiring validation
packages/opencode/test/altimate/skill-install-classifier.test.ts, packages/opencode/test/upstream/fork-feature-guards.test.ts
Tests source normalization and classification, and verifies synthetic-row routing and dialog-prefill wiring.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant DialogSkillList
  participant classifyInstallSource
  participant DialogSkillInstall
  participant installSkillDirect

  User->>DialogSkillList: Enter install source filter
  DialogSkillList->>classifyInstallSource: Classify filter
  classifyInstallSource-->>DialogSkillList: Return source kind
  DialogSkillList->>DialogSkillInstall: Select synthetic install row
  DialogSkillInstall->>installSkillDirect: Submit initial filter value
  installSkillDirect-->>DialogSkillInstall: Install via Git or absolute path
Loading

Poem

A bunny spots a repo path bright,
Adds an install row just right.
Filters hop to prompts with care,
Git or paths are routed there.
Tests nibble every edge—
🐇✨ shipped from hedge to hedge!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly matches the main change: adding typed-query install support in the /skills list.
Description check ✅ Passed The description follows the template well and covers the change, verification, screenshots note, and checklist; only the linked issue is left blank.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/skills-install-from-typed-url

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

1 similar comment
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 3 files

Re-trigger cubic

@dev-punia-altimate dev-punia-altimate left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Code Review — OpenCodeReview (Gemini) — 2 finding(s)

  • 2 anchored to a line (posted inline when the comment stream is on)
  • 0 without a line anchor
All findings (full text)

1. packages/opencode/src/plugin/tui/altimate/skill-ops.tsx (L592-L596)

[🟠 MEDIUM] Performance Issue: Unnecessary reactivity overhead

Inside this createMemo, the signal filter() is read. This means the memo depends on filter() and will re-evaluate on every keystroke. During this re-evaluation, the list.map iterates over all skills, executing detectToolReferences (which parses content using regex). This can cause noticeable UI lag during search when there are many skills.

Suggestion:
Separate the reactivity into two memos: one for computing the base list of skills() (which only updates when the underlying skills data changes), and another for handling the dynamic "Install" option that reacts to filter() changes.

  const baseOptions = createMemo<TuiDialogSelectOption<string>[]>(() => {
    const list = skills() ?? []
    const maxWidth = Math.max(0, ...list.map((s) => s.name.length))
    return list.map((skill) => {
      const tools = detectToolReferences(skill.content)
      const category = SKILL_CATEGORIES[skill.name] ?? "Other"
      const desc = skill.description?.replace(/\s+/g, " ").trim()
      return {
        title: skill.name.padEnd(maxWidth),
        description: desc,
        footer: tools.length > 0 ? tools.map((t) => `\u25b6 ${t}`).join("  ") : undefined,
        value: skill.name,
        category,
      } satisfies TuiDialogSelectOption<string>
    })
  })

  const options = createMemo<TuiDialogSelectOption<string>[]>(() => {
    const items = baseOptions()
    const q = filter().trim()
    if (classifyInstallSource(q) !== null) {
      const installOption: TuiDialogSelectOption<string> = {
        title: `Install ${q}`,
        description: "Press Enter to install from this GitHub repo, URL, or path",
        footer: undefined,
        value: INSTALL_ACTION_VALUE,
        category: "Install",
      }
      return [installOption, ...items]
    }
    return items
  })

2. packages/opencode/src/plugin/tui/altimate/skill-ops.tsx (L177-L180)

[🟠 MEDIUM] Bug: Inconsistent string normalization

The classifyInstallSource function accepts leading/trailing whitespaces and strips .git suffixes (e.g., " owner/repo "), returning a valid kind. However, installSkillDirect relies on the unmodified normalized string.
If normalized has spaces or an existing .git suffix (e.g., " owner/repo " or "owner/repo.git"), it passes the if check but generates a malformed URL like https://github.com/ owner/repo .git or https://github.com/owner/repo.git.git.

Suggestion:
Ensure the string used to construct the URL matches the cleaned format recognized by the classifier.

  const kind = classifyInstallSource(normalized)
  if (kind === "github-url" || kind === "owner-repo") {
    const cleaned = normalized.trim().replace(/\.+$/, "").replace(/\.git$/, "")
    const url = cleaned.startsWith("http") ? cleaned : `https://github.com/${cleaned}.git`
    const label = url.replace(/https?:\/\/github\.com\//, "").replace(/\.git$/, "")

Comment on lines 592 to 596
const options = createMemo<TuiDialogSelectOption<string>[]>(() => {
const list = skills() ?? []
const maxWidth = Math.max(0, ...list.map((s) => s.name.length))
return list.map((skill) => {
const items = list.map((skill) => {
const tools = detectToolReferences(skill.content)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🟠 MEDIUM] Performance Issue: Unnecessary reactivity overhead

Inside this createMemo, the signal filter() is read. This means the memo depends on filter() and will re-evaluate on every keystroke. During this re-evaluation, the list.map iterates over all skills, executing detectToolReferences (which parses content using regex). This can cause noticeable UI lag during search when there are many skills.

Suggestion:
Separate the reactivity into two memos: one for computing the base list of skills() (which only updates when the underlying skills data changes), and another for handling the dynamic "Install" option that reacts to filter() changes.

  const baseOptions = createMemo<TuiDialogSelectOption<string>[]>(() => {
    const list = skills() ?? []
    const maxWidth = Math.max(0, ...list.map((s) => s.name.length))
    return list.map((skill) => {
      const tools = detectToolReferences(skill.content)
      const category = SKILL_CATEGORIES[skill.name] ?? "Other"
      const desc = skill.description?.replace(/\s+/g, " ").trim()
      return {
        title: skill.name.padEnd(maxWidth),
        description: desc,
        footer: tools.length > 0 ? tools.map((t) => `\u25b6 ${t}`).join("  ") : undefined,
        value: skill.name,
        category,
      } satisfies TuiDialogSelectOption<string>
    })
  })

  const options = createMemo<TuiDialogSelectOption<string>[]>(() => {
    const items = baseOptions()
    const q = filter().trim()
    if (classifyInstallSource(q) !== null) {
      const installOption: TuiDialogSelectOption<string> = {
        title: `Install ${q}`,
        description: "Press Enter to install from this GitHub repo, URL, or path",
        footer: undefined,
        value: INSTALL_ACTION_VALUE,
        category: "Install",
      }
      return [installOption, ...items]
    }
    return items
  })

Comment on lines +177 to 180
const kind = classifyInstallSource(normalized)
if (kind === "github-url" || kind === "owner-repo") {
const url = normalized.startsWith("http") ? normalized : `https://github.com/${normalized}.git`
const label = url.replace(/https?:\/\/github\.com\//, "").replace(/\.git$/, "")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🟠 MEDIUM] Bug: Inconsistent string normalization

The classifyInstallSource function accepts leading/trailing whitespaces and strips .git suffixes (e.g., " owner/repo "), returning a valid kind. However, installSkillDirect relies on the unmodified normalized string.
If normalized has spaces or an existing .git suffix (e.g., " owner/repo " or "owner/repo.git"), it passes the if check but generates a malformed URL like https://github.com/ owner/repo .git or https://github.com/owner/repo.git.git.

Suggestion:
Ensure the string used to construct the URL matches the cleaned format recognized by the classifier.

  const kind = classifyInstallSource(normalized)
  if (kind === "github-url" || kind === "owner-repo") {
    const cleaned = normalized.trim().replace(/\.+$/, "").replace(/\.git$/, "")
    const url = cleaned.startsWith("http") ? cleaned : `https://github.com/${cleaned}.git`
    const label = url.replace(/https?:\/\/github\.com\//, "").replace(/\.git$/, "")

@dev-punia-altimate

Copy link
Copy Markdown
Contributor

🤖 Code Review — OpenCodeReview (Gemini) — No Issues Found

No supported files changed.

Two medium findings from the automated review pass on this PR:

1. `installSkillDirect` used the raw `normalized` input when building a
   clone URL, but `classifyInstallSource` tolerates a trailing `.git`
   suffix / whitespace / trailing dots. An input like `owner/repo.git`
   passed the classifier's `owner-repo` check and then produced
   `https://github.com/owner/repo.git.git`. Extracted the trim/strip
   logic into `normalizeInstallSource` and use it in both the classifier
   and the URL builder so the two can't disagree on shape.

2. The `options` `createMemo` read the `filter()` signal, so every
   keystroke re-executed the full `list.map` — including
   `detectToolReferences` (regex parse per skill). Split into a
   `baseOptions` memo that depends only on `skills()` and a derived
   `options` memo that composes the synthetic Install row on top. Now a
   filter change only re-checks `classifyInstallSource(q)` and prepends
   one item; `detectToolReferences` only re-runs when the underlying
   skills list changes.

Also adds a `normalizeInstallSource` unit-test suite (4 cases pinning
the `.git` / trailing-dot / whitespace behaviour) alongside the existing
classifier tests.

Verified: `bun run typecheck` clean; classifier suite 14/14 (54 expects);
fork-feature-guards 18/18 (61 expects).
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@github-actions

Copy link
Copy Markdown

Thanks for updating your PR! It now meets our contributing guidelines. 👍

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants