From c49cdd4cc8d9e69e3a1b5fd331b5583447dda73c Mon Sep 17 00:00:00 2001 From: "Beau Beauchamp, WebTigers" Date: Mon, 17 Aug 2026 02:07:19 -0400 Subject: [PATCH] Skills: one DataTables grid (browse + installed merged), pretty admin URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Datatablize /agent/skills like the Modules screen: a single server-side grid whose rows are the browse catalog merged with what's installed, so a row's Status column + action controls tell you whether it's installed. Installed skills pin to the top (then active-first, then name). - Agent_Service_Skills::datatable() — merges Tiger_Skill_Index::all() (cached; refresh on demand) with Tiger_Agent_Skills::installed() keyed by install key, filters on search, pins installed-first, paginates in PHP (the Code Area merge pattern). Rows carry installed/active + the fields to install/toggle/ remove/view-source. - View rewritten as a tigerDataTable (#sk-table): Skill · Description · Source (provenance) · Status (On/Installed/Available) · state-aware Actions (install | on-off toggle + remove | view-source), review-before-install modal, paste-URL install, rescan. ordering:false so the install pin holds. - Pretty URL /admin/settings/agent/skills via the agent module's routes.ini (a static route; canonical /agent/skills still works). Settings-nav href points at it. Interim one-off ahead of the general admin-URL normalization. - Tests: datatable merge + installed-pin + active-follows-config + guest-deny. Co-Authored-By: Claude Opus 4.8 --- modules/agent/Bootstrap.php | 2 +- modules/agent/configs/routes.ini | 17 ++ modules/agent/services/Skills.php | 76 ++++++ .../agent/views/scripts/skills/index.phtml | 258 ++++++++++-------- tests/Integration/Agent/SkillsTest.php | 47 ++++ 5 files changed, 286 insertions(+), 114 deletions(-) create mode 100644 modules/agent/configs/routes.ini diff --git a/modules/agent/Bootstrap.php b/modules/agent/Bootstrap.php index 3c63092..c9938ac 100644 --- a/modules/agent/Bootstrap.php +++ b/modules/agent/Bootstrap.php @@ -38,7 +38,7 @@ protected function _initAdminSettings() 'key' => 'agent-skills', 'label' => 'Agent Skills', 'icon' => 'fa-wand-magic-sparkles', - 'href' => '/agent/skills', + 'href' => '/admin/settings/agent/skills', // pretty alias (routes.ini); /agent/skills still works 'resource' => 'Agent_SkillsController', 'order' => 46, ]); diff --git a/modules/agent/configs/routes.ini b/modules/agent/configs/routes.ini new file mode 100644 index 0000000..e823abf --- /dev/null +++ b/modules/agent/configs/routes.ini @@ -0,0 +1,17 @@ +; SPDX-License-Identifier: BSD-3-Clause +; Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers. +; +; Pretty admin URL for the Agent Skills manager (ROUTING.md — folds into the config cascade). The +; canonical /agent/skills path keeps working for free; this is the /admin/settings// +; alias (an interim one-off ahead of the general admin-URL normalization to /admin/settings/[module]/*). +; A static route resolves to a dispatchable controller, so the RouteOverride plugin leaves it alone even +; though it sits under the reserved /admin prefix. +[production] +resources.router.routes.agentSkillsAdmin.type = "Zend_Controller_Router_Route_Static" +resources.router.routes.agentSkillsAdmin.route = "admin/settings/agent/skills" +resources.router.routes.agentSkillsAdmin.defaults.module = "agent" +resources.router.routes.agentSkillsAdmin.defaults.controller = "skills" +resources.router.routes.agentSkillsAdmin.defaults.action = "index" +[staging : production] +[testing : production] +[development : production] diff --git a/modules/agent/services/Skills.php b/modules/agent/services/Skills.php index 767647b..103247c 100644 --- a/modules/agent/services/Skills.php +++ b/modules/agent/services/Skills.php @@ -49,6 +49,82 @@ public function installed(array $params): void $this->_success(['skills' => Tiger_Agent_Skills::installed()], null); } + /** + * DataTables source — the ONE grid: the browse catalog merged with what's installed, so a row's status + + * action controls tell you whether it's installed (like the Modules screen). Installed skills are pinned + * to the top (then active-first, then by name). Search filters name/description/provenance; `refresh` + * re-scans the sources (bypass the per-source cache). Cached scans, merged + paginated in PHP (mixed + * origins, no shared DB order). + * + * @param array $params DataTables request (+ optional `refresh`) + * @return void + */ + public function datatable(array $params): void + { + if (!$this->_isAdmin()) { $this->_error('core.api.error.not_allowed'); return; } + + $dt = $this->_dtParams($params); + $search = strtolower(trim((string) $dt['search'])); + $refresh = !empty($params['refresh']); + + // Installed skills keyed by install key (key == safeKey(source__name), which installKey() mirrors). + $installed = []; + foreach (Tiger_Agent_Skills::installed() as $s) { $installed[$s['key']] = $s; } + + $items = []; + $seen = []; + // The browse catalog (per-source cached; only the sources scan hits the network, and only on refresh). + foreach (Tiger_Skill_Index::all($refresh) as $e) { + $key = Agent_Service_Skills::installKey($e); + $inst = $installed[$key] ?? null; + $items[] = $this->_row($key, (string) $e['source'], $e['name'], $e['description'], $e['sourceLabel'], + $e['repo'], $e['ref'], $e['path'], $e['url'], $inst !== null, $inst !== null && !empty($inst['active'])); + $seen[$key] = true; + } + // Installed but not in any catalog (a pasted-URL install, or a source that's since delisted). + foreach ($installed as $key => $s) { + if (isset($seen[$key])) { continue; } + $items[] = $this->_row($key, '', $s['name'], $s['description'], $s['sourceLabel'], + (string) $s['repo'], '', '', (string) $s['url'], true, !empty($s['active'])); + } + + if ($search !== '') { + $items = array_values(array_filter($items, static function ($r) use ($search) { + return strpos(strtolower($r['name'] . ' ' . $r['description'] . ' ' . $r['sourceLabel']), $search) !== false; + })); + } + + // Pin installed to the top → active-first within installed → then by name. + usort($items, static function ($a, $b) { + if ($a['installed'] !== $b['installed']) { return $a['installed'] ? -1 : 1; } + if ($a['installed'] && $a['active'] !== $b['active']) { return $a['active'] ? -1 : 1; } + return strcasecmp($a['name'], $b['name']); + }); + + $total = count($items); + $len = ($dt['length'] > 0) ? $dt['length'] : 25; + $page = array_slice($items, (int) $dt['start'], $len); + $this->_dtResponse($dt['draw'], $total, $total, $page); + } + + /** One normalized grid row (a catalog entry and/or an installed skill). */ + private function _row($key, $source, $name, $desc, $sourceLabel, $repo, $ref, $path, $url, $installed, $active): array + { + return [ + 'key' => $key, + 'source' => $source, // adapter id — needed so an Install uses the same key + 'name' => (string) $name, + 'description' => (string) $desc, + 'sourceLabel' => (string) $sourceLabel, // provenance, NOT a vouch + 'repo' => (string) $repo, + 'ref' => (string) $ref, + 'path' => (string) $path, + 'url' => (string) $url, + 'installed' => (bool) $installed, + 'active' => (bool) $active, + ]; + } + /** * Install a skill — from a browse entry (`repo`/`ref`/`path`/`name`/`source`) or a pasted `url`. * diff --git a/modules/agent/views/scripts/skills/index.phtml b/modules/agent/views/scripts/skills/index.phtml index f9333f3..1e3fc3a 100644 --- a/modules/agent/views/scripts/skills/index.phtml +++ b/modules/agent/views/scripts/skills/index.phtml @@ -2,154 +2,186 @@ // SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers. /** - * Agent Skills manager (admin shell). Browse/search the supported skill repos (Agent_Service_Skills over - * /api), install, and manage installed skills (turn on/off, remove, view source). Tiger is NOT a trust - * authority — results show PROVENANCE and you review the SKILL.md before installing (TIGERSKILLS.md). + * Agent Skills manager — ONE DataTables grid (like the Modules screen): the browse catalog merged with + * what's installed, so a row's Status + action controls tell you whether it's installed. Installed skills + * pin to the top. Data loads from Agent_Service_Skills::datatable over /api; install / toggle / remove / + * view-source are /api calls. Tiger is NOT a trust authority — the grid shows PROVENANCE and you review the + * SKILL.md before installing (TIGERSKILLS.md §2, §5). */ ?>

Agent Skills

-

Installable know-how for the AI agent. Tiger browses these repos — it does not vouch for them; review a skill before you install and turn it on.

+

Installable know-how for the AI agent. Tiger browses these repos — it does not vouch for them; review a skill's source before you install and turn it on. Installed skills are pinned to the top.

+
+
+
-
-
-
-
Browse skills
-
-
- - -
-
- -
-
-
-
Add from a GitHub URL
-
-
- - -
-
Any repo, branch, subfolder, or a link straight to a SKILL.md.
-
+
+
+ +
+ +
+
Any repo, branch, subfolder, or a link straight to a SKILL.md — not just the listed sources.
+
-
-
-
Installed
-
-
- -
+
+
+
+ + + + + + + + + + +
SkillDescriptionSourceStatusActions
+
-
-