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$/, "")

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.

@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) — 1 finding(s)

  • 1 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 (L192-L193)

[🟠 MEDIUM] There are two potential issues here:

  1. Stripping .git from HTTP URLs: Using cleaned for explicit HTTP URLs inadvertently removes the .git suffix if the user explicitly provided one (e.g., https://git.example.com/repo.git becomes https://git.example.com/repo). While GitHub tolerates this, many self-hosted Git servers require the suffix and will fail to clone.
  2. Conflating logic: Relying on cleaned.startsWith("http") can incorrectly match an owner-repo shorthand if the owner happens to be http or https (e.g., http/my-repo), resulting in an invalid URL instead of correctly cloning https://github.com/http/my-repo.git.

You can fix both by leveraging the already computed kind variable to avoid false positives and preserving the original suffix for HTTP URLs while still cleaning up any trailing dots.

Suggested change:

    const cleaned = normalizeInstallSource(normalized)
    const url = kind === "github-url" 
      ? normalized.trim().replace(/\.+$/, "") 
      : `https://github.com/${cleaned}.git`

Comment on lines +192 to +193
const cleaned = normalizeInstallSource(normalized)
const url = cleaned.startsWith("http") ? cleaned : `https://github.com/${cleaned}.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] There are two potential issues here:

  1. Stripping .git from HTTP URLs: Using cleaned for explicit HTTP URLs inadvertently removes the .git suffix if the user explicitly provided one (e.g., https://git.example.com/repo.git becomes https://git.example.com/repo). While GitHub tolerates this, many self-hosted Git servers require the suffix and will fail to clone.
  2. Conflating logic: Relying on cleaned.startsWith("http") can incorrectly match an owner-repo shorthand if the owner happens to be http or https (e.g., http/my-repo), resulting in an invalid URL instead of correctly cloning https://github.com/http/my-repo.git.

You can fix both by leveraging the already computed kind variable to avoid false positives and preserving the original suffix for HTTP URLs while still cleaning up any trailing dots.

Suggested change:

Suggested change
const cleaned = normalizeInstallSource(normalized)
const url = cleaned.startsWith("http") ? cleaned : `https://github.com/${cleaned}.git`
const cleaned = normalizeInstallSource(normalized)
const url = kind === "github-url"
? normalized.trim().replace(/\.+$/, "")
: `https://github.com/${cleaned}.git`

@dev-punia-altimate

Copy link
Copy Markdown
Contributor

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

No supported files changed.

@ralphstodomingo

ralphstodomingo commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Note

AI-assisted code review. Produced by Claude (Opus 4.8) running an adversarial multi-agent
workflow against b8e7e64, then verified live in a terminal, posted from @ralphstodomingo's
account. Read the findings as claims to check, not as vetted conclusions.

Method: a recon pass first pinned the DialogSelect / DialogPrompt / dialog.replace API
contracts this PR depends on. Five independent finders then swept separate lenses (does-it-work
end-to-end, classifier correctness, module-state lifecycle, UX + the perf claim, test quality).
Every candidate finding faced three adversarial verifiers instructed to refute it, majority-refute
dropping it — 2 of 5 candidates were killed that way. Finally the TUI was driven live in a PTY
(pyte screen emulation) against b8e7e64, so the behavioral findings below are reproduced
end-to-end, not inferred from reading.

Verdict: mergeable with fixes. The core design is sound — one exported classifyInstallSource
(skill-ops.tsx:55) consumed by both the installer (:186) and the list's synthetic-row gate
(:638) genuinely removes the UI/installer drift the old q.includes("/") check caused, and the
sentinel is correctly filtered out of onMove (:662) and showActions (:721) so a highlighted
Install row can't leak into the action picker. The non-mutating memo (return [installOption, ...items],
:646) is right for Solid's double-eval. Live drive confirms the feature works: the synthetic
row renders and survives DialogSelect's internal filter for anthropics/skills and
httpie/httpie, shows no row for dbt-develop, and prefills the install dialog. Four issues remain
— one correctness bug, one merge-drop guard gap, a comment inaccuracy, and a slash-command collision
that makes the feature unreachable by the documented path.

Findings

medium — installSkillDirect discards the computed kind and re-derives the clone-URL branch from a bare http prefix test

packages/opencode/src/plugin/tui/altimate/skill-ops.tsx:193

kind is computed at :186 and is the exact, correct discriminant, but :193 throws it away:

const url = cleaned.startsWith("http") ? cleaned : `https://github.com/${cleaned}.git`

The classifier tests the scheme-qualified prefixes http:// / https:// (:58); :193 tests
only the bare substring http. OWNER_REPO_REGEX (:45) admits owners starting with those four
letters, so httpie/httpie classifies as owner-repo at :186, takes the "already a URL" path at
:193, and https://github.com/ is never prepended.

Reproduced live. Type httpie/httpie into the fork's /skills dialog, Enter, Enter:

Cloning httpie/httpie...
Install failed: Failed to clone: fatal: repository 'httpie/httpie' does not exist

Control — same owner/repo shape, non-http owner — succeeds, isolating the cause to the
prefix drift rather than anything environmental:

Cloning anthropics/skills...  Found 18 skill(s). Installing...  Installed 1/18: …

Scope is genuinely small — it needs an owner literally prefixed http (httpie/httpie,
http-party/http-server, httpwg/http-extensions) and the shorthand form (pasting the full URL
works), and it fails loudly. The flaw predates the PR (the removed line used
normalized.startsWith("http")), but :193 is a line this PR rewrote, the new row at :638-646
now advertises exactly these inputs as installable, and it's precisely the classifier/installer
drift the header comment at :37-43 says the shared classifier exists to prevent — the right
answer is in hand two lines up and unused.

Fix (verified live + all 32 tests still pass):

const url = kind === "github-url" ? cleaned : `https://github.com/${cleaned}.git`

With this applied, httpie/httpie now clones and fails only because it genuinely isn't a skills
repo — the correct outcome:

Cloning httpie/httpie...  Scanning for skills...
Install failed: No SKILL.md files found in httpie/httpie

Add a classifier test with an http-prefixed owner to pin it.

medium — /skills + Enter opens the upstream dialog, not this one, so the new Install row is unreachable by the documented path

packages/tui/src/component/prompt/index.tsx:559-562 and packages/opencode/src/plugin/tui/altimate/skill-ops.tsx:731-736

Two commands register the same slash trigger:

  • prompt.skills — upstream, category "Prompt"
  • altimate.skill.list — this PR's plugin, category "Altimate"

Driving it live: typing /skills and pressing Enter opens the upstream dialog
(title="Skills", placeholder="Search skills...", packages/tui/src/component/dialog-skill.tsx:35),
where typing a URL yields "No results found." You must press Down first to reach this PR's
dialog (the one with placeholder="Search skills, or type a repo/URL…", skill-ops.tsx:655).

This is pre-existing on main — both slashName: "skills" registrations already exist there —
so it is explicitly not a defect this PR introduces. Flagging it because it undercuts the PR's
headline goal: the smoke plan in the description ("/skills → type
https://github.com/anthropics/skills.git → Enter opens install dialog") cannot work as written;
plain /skills+Enter lands on the upstream dialog and produces exactly the "filters to zero results,
no obvious next action" dead end this PR set out to remove. Worth a separate ticket, but this PR's
discoverability win doesn't land for anyone reaching the list via /skills.

medium — the merge-drop guard doesn't cover the block that makes the Install row exist

packages/opencode/test/upstream/fork-feature-guards.test.ts:206-215

Deleting the synthetic-row block from the options memo (skill-ops.tsx:637-647) removes the
feature entirely — the row never renders and the onSelect routing at :666 becomes dead code —
yet all 18 guards still pass (confirmed by real mutation test, not simulation). Two of the five
assertions are inert as written: /classifyInstallSource/ (:209) is satisfied by the definition
at :55 and the installer call at :186; /INSTALL_ACTION_VALUE/ (:210) by the const at :69.
The proximity regexes at :212/:214-215 do have teeth (they pin onSelect routing and prefill
props), so this isn't zero detection — the gap is specific to the memo block, which is both the
load-bearing site and the one most likely to get clobbered during an upstream re-extraction.

Fix — one added assertion, verified in the real runner (18 pass on correct source; 17 pass / 1 fail
when the feature is deleted):

expect(skill).toMatch(/classifyInstallSource\(q\)[\s\S]{0,300}value: INSTALL_ACTION_VALUE/)

(The gap between those two anchors is 247 chars, so the bound must be ≥250 — a tighter {0,200}
fails against the correct source too.)

low — comments cite dbt/snowflake as the pinned regression, but it classifies as owner-repo and is never asserted

packages/opencode/test/altimate/skill-install-classifier.test.ts:7, 37-41 and packages/opencode/src/plugin/tui/altimate/skill-ops.tsx:40-43

The test header (:4-8), the inline comment at :38, and skill-ops.tsx:41-43 all name
dbt/snowflake as the query the classifier now rejects ("search terms, relative paths, ~"). But
classifyInstallSource("dbt/snowflake") returns "owner-repo"OWNER_REPO_REGEX (:45) matches
any clean two-segment string. The test titled "rejects multi-slash paths…" only asserts the
three-segment owner/repo/subpath and dbt/snowflake/thing (:40-41), which fail for an unrelated
reason (the second slash). The one case named twice as motivating is the one case not asserted and
not actually rejected.

This is a documentation defect, not functional: accepting clean owner/repo is the feature (a
two-segment string is indistinguishable from a two-token search without a network call), and skill
names can't contain a slash (skill-ops.tsx:125), so no search result is hijacked. Fix: correct
the comments to say two-segment inputs are intentionally treated as owner/repo shorthand, reserve
the "search terms" rejection claim for the three-segment / relative / ~ cases the code actually
rejects, and optionally pin expect(classifyInstallSource("dbt/snowflake")).toBe("owner-repo").

What I could not verify

  • Only the owner/repo and github-URL install paths were driven live. The absolute-path branch
    and the ctrl+n/ctrl+i prefill plumbing were exercised by reading only.
  • The module-scope currentFilter lifecycle (potential stale-text leak across dialogs) was reviewed
    statically; I didn't construct a live sequence that trips it.
  • No claim either way on the baseOptions/options per-keystroke perf split — structurally correct
    (baseOptions reads skills(), options reads filter()) but unmeasured.

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.

3 participants