Skip to content

feat(extensions): manual source binding for locally-installed skills#56

Merged
RealZST merged 3 commits into
mainfrom
feat/manual-source-binding
May 15, 2026
Merged

feat(extensions): manual source binding for locally-installed skills#56
RealZST merged 3 commits into
mainfrom
feat/manual-source-binding

Conversation

@RealZST
Copy link
Copy Markdown
Owner

@RealZST RealZST commented May 15, 2026

Summary

  • Let users bind a GitHub repo to a locally-scanned skill that has no source detected, opting it into Check Updates / Update.
  • Replace the "No source" input with a three-state pill (CTA โ†’ editing โ†’ linked) that normalizes URL paste forms and rejects non-github / malformed input before write.
  • Wire the new install_type = "manual" through check_updates (bucket + skill-name verification) and update_extension (whitelist) so manually-bound rows are first-class in the update pipeline.

How it works

  • service::bind_pack is the single write path for the detail panel's source field. It synthesizes install_meta { install_type: "manual", url, revision: None } for sourceless rows and refreshes/clears it when the user edits/unbinds. Real git / marketplace install_meta is never touched โ€” a user-typed pack is just a grouping hint when a real install exists.
  • normalize_pack accepts: bare owner/repo, https://github.com/owner/repo[.git][/tree/main], schemeless github.com/..., and git@github.com:owner/repo.git. Non-github paste forms (e.g. gitlab.com/foo) are rejected by the validator so we never synthesize a wrong https://github.com/... URL.
  • migrate_pack_to_manual_meta is invoked at the top of every check_updates; idempotent, opts in pre-feature rows that had pack typed before this flow existed.
  • check_updates clones the bound repo and verifies the skill name actually lives in it. If not (typo / wrong repo), surfaces RemovedFromRepo immediately rather than waiting for the user to click Update and hit the same check.
  • Frontend pill is a three-state machine: empty โ†’ + Bind source CTA โ†’ click โ†’ input (autofocus, Enter commits with toast on invalid, Esc cancels, blur silently dismisses) โ†’ success โ†’ link chip + pencil to re-edit.
  • New INFO-section row surfaces UpdateStatus::Error (network errors, missing repos, etc.) โ€” previously dropped silently in the UI.

Test plan

  • cargo test -p hk-core โ€” 432 / 432 pass (+18 new tests covering normalize, validate, bind_pack matrix, migrate)
  • npm run test โ€” 164 / 164 vitest (+15 helper tests for normalizePack / isValidPackFormat / groupOwnerRepo incl. CLI fallback)
  • npm run lint โ€” clean
  • cargo check --workspace โ€” all 4 crates compile
  • E2E manual: created a local stub webapp-testing/SKILL.md with placeholder content โ†’ bound to anthropics/skills โ†’ Check Updates correctly flagged update_available (not removed_from_repo, since the skill exists in that repo) โ†’ Update succeeded; version chip appeared; local SKILL.md was replaced with the real webapp-testing content from the repo.
  • Edge cases manually verified:
    • 5 URL paste forms all normalize to owner/repo
    • 6 invalid inputs (no slash / multi-slash / spaces / non-github URL / gitlab.com/foo / google.com/foo) all show toast.warning and stay in edit
    • Unbind via empty input clears install_meta + drops stale removed_from_repo warning
    • Cmd+A inside input doesn't bubble to document
    • Detail panel survives binding (selectedId re-resolved after groupKey change)
    • Pencil click reliably enters edit mode (fixed a useEffect dep bug that was racing the edit toggle)

Refs #39 (A3).

๐Ÿค– Generated with Claude Code

RealZST and others added 3 commits May 15, 2026 15:19
Add service::bind_pack as the single write path for the detail panel's
source field. Synthesizes a "manual" install_meta when the user binds a
locally-scanned skill to a GitHub repo, leaving real git/marketplace
install_meta untouched so a user-typed pack only opts in sourceless rows
without overriding HK-managed installs.

normalize_pack accepts owner/repo, HTTP(S) URLs, schemeless github.com
paths, and SSH clone URLs, delegating URL parsing to the existing
scanner::extract_pack_from_url. A github-only gate prevents non-github
paste forms (gitlab.com/foo, host.tld/x) from being synthesized into
wrong github URLs.

migrate_pack_to_manual_meta provides a one-shot path for rows that had
pack set before this feature shipped โ€” idempotent on subsequent calls,
safe to invoke at the top of every check_updates.

Both desktop and web settings handlers now funnel update_pack /
batch_update_pack through service::bind_pack.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire the new "manual" install_type into the update pipeline:

- check_updates buckets "manual" alongside "git" / "marketplace" so
  manually-bound skills participate in update detection.
- After clone, verify the named skill actually lives in the repo
  (mirrors the marketplace-skill subpath check). A user can paste any
  GitHub URL into the source field โ€” if their skill name doesn't exist
  in that repo, surface RemovedFromRepo immediately instead of waiting
  for the user to click Update.
- update_extension whitelist accepts "manual" so the update flow
  succeeds for manually-bound skills. After update the row's
  install_type stays "manual" so the user can still unbind later.
- Call migrate_pack_to_manual_meta at the top of check_updates so
  legacy pre-feature rows (pack present, no install_meta) opt in
  automatically on the next user-triggered check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the always-editable "No source" input with a three-state pill:

- Empty: "+ Bind source" CTA button.
- Editing: input with autofocus; Enter commits with validation toast on
  invalid input, Esc cancels, blur silently dismisses. Client-side
  normalizes URLs to owner/repo and validates before write โ€” mirrors
  the backend's normalize_pack / is_valid_pack_format.
- Bound: GitHub link chip with pencil icon to re-edit. Pill resolves
  the link text via groupOwnerRepo (pack โ†’ source.url โ†’
  install_meta.url โ†’ CLI child install_meta) so it covers the union
  of every place source info can live.

Drop the redundant globe-link row from the INFO section โ€” the pill now
covers that display. Add an error row to surface check_updates failures
(network errors, missing repos) so users can see why a check failed
instead of the Error status being dropped silently.

extension-store.updatePack now refetches extensions after the API call
so install_meta side effects are reflected; drops stale update statuses
for affected rows so warnings don't outlive the binding they referred
to; re-resolves selectedId via stable instance ids so the detail panel
survives the extensionGroupKey change that binding triggers.

Fix an unrelated useEffect dep bug where listing the full `group`
object in the dep array caused the effect to re-fire on every render
(grouped() rebuilds the object reference each time) and silently exit
edit mode mid-edit.

Cmd+A is intercepted in the source input so it selects only the
input contents instead of bubbling to the document and selecting the
whole page (a Tauri WebView quirk that affects any form-less input).

i18n: 5 new keys (bindSource, bindSourcePlaceholder, editSource,
bindSourceSuccess, bindSourceInvalid) + updateCheckFailed for the new
error row. Drop unused noSource key.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@RealZST RealZST merged commit f67e821 into main May 15, 2026
3 checks passed
@RealZST RealZST deleted the feat/manual-source-binding branch May 15, 2026 12:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant