botmux 3.12.0 → 3.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/dist/.runtime-build-id +1 -1
  2. package/dist/adapters/backend/riff-backend.d.ts +18 -0
  3. package/dist/adapters/backend/riff-backend.d.ts.map +1 -1
  4. package/dist/adapters/backend/riff-backend.js +143 -0
  5. package/dist/adapters/backend/riff-backend.js.map +1 -1
  6. package/dist/adapters/cli/kimi.d.ts.map +1 -1
  7. package/dist/adapters/cli/kimi.js +22 -8
  8. package/dist/adapters/cli/kimi.js.map +1 -1
  9. package/dist/adapters/cli/pi.d.ts.map +1 -1
  10. package/dist/adapters/cli/pi.js +3 -1
  11. package/dist/adapters/cli/pi.js.map +1 -1
  12. package/dist/bot-registry.d.ts.map +1 -1
  13. package/dist/bot-registry.js +4 -1
  14. package/dist/bot-registry.js.map +1 -1
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +65 -27
  17. package/dist/cli.js.map +1 -1
  18. package/dist/core/command-handler.d.ts.map +1 -1
  19. package/dist/core/command-handler.js +16 -2
  20. package/dist/core/command-handler.js.map +1 -1
  21. package/dist/core/dashboard-ipc-server.js +1 -1
  22. package/dist/core/dashboard-ipc-server.js.map +1 -1
  23. package/dist/core/dashboard-rows.d.ts +17 -1
  24. package/dist/core/dashboard-rows.d.ts.map +1 -1
  25. package/dist/core/dashboard-rows.js +13 -1
  26. package/dist/core/dashboard-rows.js.map +1 -1
  27. package/dist/core/maintenance.d.ts.map +1 -1
  28. package/dist/core/maintenance.js +5 -0
  29. package/dist/core/maintenance.js.map +1 -1
  30. package/dist/core/skills/cli-admin-command.d.ts.map +1 -1
  31. package/dist/core/skills/cli-admin-command.js +142 -3
  32. package/dist/core/skills/cli-admin-command.js.map +1 -1
  33. package/dist/core/skills/frontmatter.d.ts.map +1 -1
  34. package/dist/core/skills/frontmatter.js +44 -7
  35. package/dist/core/skills/frontmatter.js.map +1 -1
  36. package/dist/core/skills/im-command.d.ts +6 -0
  37. package/dist/core/skills/im-command.d.ts.map +1 -1
  38. package/dist/core/skills/im-command.js +34 -7
  39. package/dist/core/skills/im-command.js.map +1 -1
  40. package/dist/core/skills/install-command.d.ts +4 -0
  41. package/dist/core/skills/install-command.d.ts.map +1 -0
  42. package/dist/core/skills/install-command.js +89 -0
  43. package/dist/core/skills/install-command.js.map +1 -0
  44. package/dist/core/skills/package.d.ts.map +1 -1
  45. package/dist/core/skills/package.js +10 -3
  46. package/dist/core/skills/package.js.map +1 -1
  47. package/dist/core/skills/policy.d.ts +5 -1
  48. package/dist/core/skills/policy.d.ts.map +1 -1
  49. package/dist/core/skills/policy.js +66 -3
  50. package/dist/core/skills/policy.js.map +1 -1
  51. package/dist/core/skills/references.d.ts +9 -1
  52. package/dist/core/skills/references.d.ts.map +1 -1
  53. package/dist/core/skills/references.js +35 -2
  54. package/dist/core/skills/references.js.map +1 -1
  55. package/dist/core/skills/registry-paths.d.ts +1 -0
  56. package/dist/core/skills/registry-paths.d.ts.map +1 -1
  57. package/dist/core/skills/registry-paths.js +3 -0
  58. package/dist/core/skills/registry-paths.js.map +1 -1
  59. package/dist/core/skills/session-resolver.d.ts +2 -1
  60. package/dist/core/skills/session-resolver.d.ts.map +1 -1
  61. package/dist/core/skills/session-resolver.js +1 -0
  62. package/dist/core/skills/session-resolver.js.map +1 -1
  63. package/dist/core/skills/session-runtime.d.ts.map +1 -1
  64. package/dist/core/skills/session-runtime.js +2 -0
  65. package/dist/core/skills/session-runtime.js.map +1 -1
  66. package/dist/core/skills/sources.d.ts +23 -0
  67. package/dist/core/skills/sources.d.ts.map +1 -1
  68. package/dist/core/skills/sources.js +128 -12
  69. package/dist/core/skills/sources.js.map +1 -1
  70. package/dist/core/skills/types.d.ts +22 -1
  71. package/dist/core/skills/types.d.ts.map +1 -1
  72. package/dist/core/types.d.ts +2 -0
  73. package/dist/core/types.d.ts.map +1 -1
  74. package/dist/core/types.js.map +1 -1
  75. package/dist/core/worker-pool.d.ts.map +1 -1
  76. package/dist/core/worker-pool.js +52 -1
  77. package/dist/core/worker-pool.js.map +1 -1
  78. package/dist/dashboard/bot-onboarding.d.ts.map +1 -1
  79. package/dist/dashboard/bot-onboarding.js +2 -114
  80. package/dist/dashboard/bot-onboarding.js.map +1 -1
  81. package/dist/dashboard/connector-api.d.ts.map +1 -1
  82. package/dist/dashboard/connector-api.js +66 -6
  83. package/dist/dashboard/connector-api.js.map +1 -1
  84. package/dist/dashboard/public-redact.d.ts.map +1 -1
  85. package/dist/dashboard/public-redact.js +3 -0
  86. package/dist/dashboard/public-redact.js.map +1 -1
  87. package/dist/dashboard/skill-install-request.js +10 -11
  88. package/dist/dashboard/skill-install-request.js.map +1 -1
  89. package/dist/dashboard/skill-pack-response.d.ts +15 -0
  90. package/dist/dashboard/skill-pack-response.d.ts.map +1 -0
  91. package/dist/dashboard/skill-pack-response.js +32 -0
  92. package/dist/dashboard/skill-pack-response.js.map +1 -0
  93. package/dist/dashboard/web/connectors-page.d.ts +69 -0
  94. package/dist/dashboard/web/connectors-page.d.ts.map +1 -1
  95. package/dist/dashboard/web/connectors-page.js +47 -7
  96. package/dist/dashboard/web/connectors-page.js.map +1 -1
  97. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  98. package/dist/dashboard/web/i18n.js +301 -26
  99. package/dist/dashboard/web/i18n.js.map +1 -1
  100. package/dist/dashboard/web/preferences.d.ts +4 -2
  101. package/dist/dashboard/web/preferences.d.ts.map +1 -1
  102. package/dist/dashboard/web/preferences.js +14 -6
  103. package/dist/dashboard/web/preferences.js.map +1 -1
  104. package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
  105. package/dist/dashboard/web/sessions-page.js +99 -3
  106. package/dist/dashboard/web/sessions-page.js.map +1 -1
  107. package/dist/dashboard/web/sessions.d.ts +3 -1
  108. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  109. package/dist/dashboard/web/sessions.js +13 -6
  110. package/dist/dashboard/web/sessions.js.map +1 -1
  111. package/dist/dashboard/web/skills/bot-assignments-tab.d.ts +27 -0
  112. package/dist/dashboard/web/skills/bot-assignments-tab.d.ts.map +1 -0
  113. package/dist/dashboard/web/skills/bot-assignments-tab.js +257 -0
  114. package/dist/dashboard/web/skills/bot-assignments-tab.js.map +1 -0
  115. package/dist/dashboard/web/skills/delivery-settings-tab.d.ts +25 -0
  116. package/dist/dashboard/web/skills/delivery-settings-tab.d.ts.map +1 -0
  117. package/dist/dashboard/web/skills/delivery-settings-tab.js +20 -0
  118. package/dist/dashboard/web/skills/delivery-settings-tab.js.map +1 -0
  119. package/dist/dashboard/web/skills/shared.d.ts +86 -0
  120. package/dist/dashboard/web/skills/shared.d.ts.map +1 -0
  121. package/dist/dashboard/web/skills/shared.js +178 -0
  122. package/dist/dashboard/web/skills/shared.js.map +1 -0
  123. package/dist/dashboard/web/skills/skill-library-tab.d.ts +75 -0
  124. package/dist/dashboard/web/skills/skill-library-tab.d.ts.map +1 -0
  125. package/dist/dashboard/web/skills/skill-library-tab.js +108 -0
  126. package/dist/dashboard/web/skills/skill-library-tab.js.map +1 -0
  127. package/dist/dashboard/web/skills/skill-packs-tab.d.ts +21 -0
  128. package/dist/dashboard/web/skills/skill-packs-tab.d.ts.map +1 -0
  129. package/dist/dashboard/web/skills/skill-packs-tab.js +185 -0
  130. package/dist/dashboard/web/skills/skill-packs-tab.js.map +1 -0
  131. package/dist/dashboard/web/skills/types.d.ts +95 -0
  132. package/dist/dashboard/web/skills/types.d.ts.map +1 -0
  133. package/dist/dashboard/web/skills/types.js +2 -0
  134. package/dist/dashboard/web/skills/types.js.map +1 -0
  135. package/dist/dashboard/web/skills/use-skills-data.d.ts +31 -0
  136. package/dist/dashboard/web/skills/use-skills-data.d.ts.map +1 -0
  137. package/dist/dashboard/web/skills/use-skills-data.js +86 -0
  138. package/dist/dashboard/web/skills/use-skills-data.js.map +1 -0
  139. package/dist/dashboard/web/skills-page.d.ts +34 -31
  140. package/dist/dashboard/web/skills-page.d.ts.map +1 -1
  141. package/dist/dashboard/web/skills-page.js +321 -176
  142. package/dist/dashboard/web/skills-page.js.map +1 -1
  143. package/dist/dashboard/webhook-routes.d.ts +18 -0
  144. package/dist/dashboard/webhook-routes.d.ts.map +1 -1
  145. package/dist/dashboard/webhook-routes.js +35 -1
  146. package/dist/dashboard/webhook-routes.js.map +1 -1
  147. package/dist/dashboard-web/app.js +1 -1
  148. package/dist/dashboard-web/chunks/{bot-defaults-page-USC6EZXP.js → bot-defaults-page-HL655CNR.js} +1 -1
  149. package/dist/dashboard-web/chunks/{chunk-KOPISPHN.js → chunk-5PAYQ3KT.js} +1 -1
  150. package/dist/dashboard-web/chunks/{chunk-RXRJB5SO.js → chunk-5ZL7NLR5.js} +1 -1
  151. package/dist/dashboard-web/chunks/{chunk-A7VC4BXR.js → chunk-BFUTTYVK.js} +1 -1
  152. package/dist/dashboard-web/chunks/chunk-ELL53D62.js +55 -0
  153. package/dist/dashboard-web/chunks/connectors-page-3QQACF23.js +1 -0
  154. package/dist/dashboard-web/chunks/{groups-page-C6NSDTIN.js → groups-page-SDZMJ7UV.js} +1 -1
  155. package/dist/dashboard-web/chunks/{insights-page-UTOVYLLY.js → insights-page-ZZ3JEJ6L.js} +1 -1
  156. package/dist/dashboard-web/chunks/{monitor-room-UKTJYTA2.js → monitor-room-ROJ5GZ4N.js} +1 -1
  157. package/dist/dashboard-web/chunks/{monitoring-page-Q3CEZIVX.js → monitoring-page-IATV7KJF.js} +1 -1
  158. package/dist/dashboard-web/chunks/{office-page-GYCZOOVJ.js → office-page-TYO5OIIT.js} +1 -1
  159. package/dist/dashboard-web/chunks/{overview-page-W2VBJ6OH.js → overview-page-VJUULZF2.js} +1 -1
  160. package/dist/dashboard-web/chunks/{roles-page-U4SHER4K.js → roles-page-7RJJKZGK.js} +1 -1
  161. package/dist/dashboard-web/chunks/{schedules-page-TNPW3N3Q.js → schedules-page-JTEJ5IXW.js} +1 -1
  162. package/dist/dashboard-web/chunks/sessions-page-KD7HDT3U.js +8 -0
  163. package/dist/dashboard-web/chunks/{settings-page-VKW5ZK5K.js → settings-page-7CTTFBRS.js} +1 -1
  164. package/dist/dashboard-web/chunks/skills-page-57NZECJ3.js +4 -0
  165. package/dist/dashboard-web/chunks/{team-federation-page-OMFGGBT2.js → team-federation-page-6YYIMJG3.js} +1 -1
  166. package/dist/dashboard-web/chunks/{v3-page-PLXWK4KO.js → v3-page-ZGKBSBK3.js} +1 -1
  167. package/dist/dashboard-web/chunks/{whiteboards-page-NJQOEO2P.js → whiteboards-page-AACJF6DB.js} +1 -1
  168. package/dist/dashboard-web/style.css +1623 -160
  169. package/dist/dashboard.js +200 -16
  170. package/dist/dashboard.js.map +1 -1
  171. package/dist/i18n/en.d.ts.map +1 -1
  172. package/dist/i18n/en.js +2 -0
  173. package/dist/i18n/en.js.map +1 -1
  174. package/dist/i18n/zh.d.ts.map +1 -1
  175. package/dist/i18n/zh.js +2 -0
  176. package/dist/i18n/zh.js.map +1 -1
  177. package/dist/index-daemon.js +6 -1
  178. package/dist/index-daemon.js.map +1 -1
  179. package/dist/services/connector-store.d.ts +14 -1
  180. package/dist/services/connector-store.d.ts.map +1 -1
  181. package/dist/services/connector-store.js.map +1 -1
  182. package/dist/services/connector-topic-template.d.ts +7 -0
  183. package/dist/services/connector-topic-template.d.ts.map +1 -0
  184. package/dist/services/connector-topic-template.js +210 -0
  185. package/dist/services/connector-topic-template.js.map +1 -0
  186. package/dist/services/project-scanner.d.ts +33 -0
  187. package/dist/services/project-scanner.d.ts.map +1 -1
  188. package/dist/services/project-scanner.js +40 -1
  189. package/dist/services/project-scanner.js.map +1 -1
  190. package/dist/services/skill-pack-store.d.ts +50 -0
  191. package/dist/services/skill-pack-store.d.ts.map +1 -0
  192. package/dist/services/skill-pack-store.js +270 -0
  193. package/dist/services/skill-pack-store.js.map +1 -0
  194. package/dist/services/skill-registry-store.d.ts +8 -0
  195. package/dist/services/skill-registry-store.d.ts.map +1 -1
  196. package/dist/services/skill-registry-store.js +100 -68
  197. package/dist/services/skill-registry-store.js.map +1 -1
  198. package/dist/services/todo-state.d.ts +74 -0
  199. package/dist/services/todo-state.d.ts.map +1 -0
  200. package/dist/services/todo-state.js +351 -0
  201. package/dist/services/todo-state.js.map +1 -0
  202. package/dist/setup/agent-preset.d.ts.map +1 -1
  203. package/dist/setup/agent-preset.js +2 -0
  204. package/dist/setup/agent-preset.js.map +1 -1
  205. package/dist/setup/owner-identity.d.ts +47 -0
  206. package/dist/setup/owner-identity.d.ts.map +1 -0
  207. package/dist/setup/owner-identity.js +196 -0
  208. package/dist/setup/owner-identity.js.map +1 -0
  209. package/dist/setup/register-app.d.ts +4 -3
  210. package/dist/setup/register-app.d.ts.map +1 -1
  211. package/dist/setup/register-app.js.map +1 -1
  212. package/dist/setup/setup-args.d.ts +1 -1
  213. package/dist/setup/setup-args.d.ts.map +1 -1
  214. package/dist/setup/setup-args.js +7 -3
  215. package/dist/setup/setup-args.js.map +1 -1
  216. package/dist/types.d.ts +11 -0
  217. package/dist/types.d.ts.map +1 -1
  218. package/dist/utils/child-env.d.ts +25 -0
  219. package/dist/utils/child-env.d.ts.map +1 -1
  220. package/dist/utils/child-env.js +47 -0
  221. package/dist/utils/child-env.js.map +1 -1
  222. package/dist/worker.js +55 -16
  223. package/dist/worker.js.map +1 -1
  224. package/dist/workflows/definition.d.ts +44 -44
  225. package/package.json +1 -1
  226. package/dist/dashboard-web/chunks/chunk-M6BHWQFO.js +0 -51
  227. package/dist/dashboard-web/chunks/connectors-page-4Z76HWRH.js +0 -1
  228. package/dist/dashboard-web/chunks/sessions-page-YWWDDV57.js +0 -6
  229. package/dist/dashboard-web/chunks/skills-page-ZCEDFGZZ.js +0 -4
@@ -1,53 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
3
3
  import { createPortal } from 'react-dom';
4
4
  import { FieldTitle, Html, LoadingState, RefreshIconButton, SectionHeader } from './dashboard-components.js';
5
5
  import { botAvatarHtml } from './ui.js';
6
6
  import { useT } from './react-hooks.js';
7
7
  import { mountReactPage } from './react-mount.js';
8
+ import { SkillPacksTab } from './skills/skill-packs-tab.js';
9
+ import { detectSourceType, SkillLibraryTab } from './skills/skill-library-tab.js';
10
+ import { BotAssignmentsTab } from './skills/bot-assignments-tab.js';
11
+ import { DeliverySettingsTab } from './skills/delivery-settings-tab.js';
12
+ import { buildSkillGraph, danglingSkillNames, discoveryGroupKey, mergeBotAssignmentSelectors, packIds, policyConfigured, priorityNames, selectInstallCandidates, sourceLabel, } from './skills/shared.js';
13
+ import { useSkillsData } from './skills/use-skills-data.js';
8
14
  const INSTALLED_SKILLS_ROWS_PER_PAGE = 2;
9
- function nativeLibraryLabel(path, tr) {
10
- const p = String(path ?? '').replace(/\\/g, '/');
11
- if (p.includes('/.codex/skills/'))
12
- return tr('skills.sourceCodex');
13
- if (p.includes('/.claude/skills/'))
14
- return tr('skills.sourceClaude');
15
- if (p.includes('/.trae/skills/'))
16
- return tr('skills.sourceTrae');
17
- if (p.includes('/.cursor/skills/'))
18
- return tr('skills.sourceCursor');
19
- if (p.includes('/.gemini/skills/'))
20
- return tr('skills.sourceGemini');
21
- if (p.includes('/.config/opencode/skills/'))
22
- return tr('skills.sourceOpenCode');
23
- return null;
24
- }
25
- function sourceLabel(skill, tr) {
26
- const source = skill.source ?? {};
27
- if (source.type === 'github')
28
- return `github:${source.owner}/${source.repo}/${source.path ?? ''}`;
29
- if (source.type === 'git')
30
- return `${source.url ?? 'git'}#${source.path ?? ''}`;
31
- if (source.type === 'local-link')
32
- return nativeLibraryLabel(source.path, tr) ?? tr('skills.sourceLocalLink');
33
- if (source.type === 'local-copy')
34
- return tr('skills.sourceBotmuxCopy');
35
- return String(source.type ?? 'unknown');
36
- }
37
- function priorityNames(policy) {
38
- return (policy?.include ?? [])
39
- .filter(item => item.startsWith('skill:'))
40
- .map(item => item.slice('skill:'.length));
41
- }
42
- function policyReferenceCount(policy) {
43
- return priorityNames(policy).length;
44
- }
45
- function policyConfigured(policy) {
46
- return priorityNames(policy).length > 0;
47
- }
48
- function discoveryGroupKey(group) {
49
- return `${group.cliId}\n${group.rootDir}`;
50
- }
51
15
  function installedSkillsColumnCount(width) {
52
16
  if (width >= 1600)
53
17
  return 4;
@@ -74,17 +38,46 @@ async function jsonRequest(url, init) {
74
38
  function statusClass(status) {
75
39
  return `oncall-status${status ? ` ${status.ok ? 'hint-ok' : 'hint-warn-inline'}` : ''}`;
76
40
  }
77
- function SkillSegmented(props) {
41
+ export function SkillSegmented(props) {
78
42
  const current = props.options.find(option => option.value === props.value);
79
43
  return (_jsxs("div", { className: "skills-segmented-control", children: [_jsx("div", { className: "segmented skills-segmented", role: "group", children: props.options.map(option => (_jsx("button", { type: "button", className: props.value === option.value ? 'active' : '', "aria-pressed": props.value === option.value ? 'true' : 'false', title: typeof option.help === 'string' ? option.help : undefined, disabled: props.disabled, onClick: () => props.onChange(option.value), children: option.label }, option.value))) }), current?.help ? _jsx("span", { className: "skills-setting-hint", children: current.help }) : null] }));
80
44
  }
81
45
  export function SkillsInstallPanel(props) {
82
46
  const tr = useT();
83
47
  const selectionDialogRef = useRef(null);
48
+ const sourceHelpRef = useRef(null);
49
+ const sourceHelpId = useId();
50
+ const sourceInputId = `${sourceHelpId}-input`;
51
+ const sourceHelpTitleId = `${sourceHelpId}-title`;
52
+ const [sourceHelpOpen, setSourceHelpOpen] = useState(false);
84
53
  const candidates = props.installCandidates ?? [];
85
54
  const selectedInstallSkills = props.selectedInstallSkills ?? new Set();
86
55
  const allSelected = candidates.length > 0 && candidates.every(candidate => selectedInstallSkills.has(candidate.name));
87
56
  const busy = props.installBusy || props.installDiscovering;
57
+ const sourceType = detectSourceType(props.installSource);
58
+ const sourceTypeLabel = sourceType === 'github'
59
+ ? tr('skills.sourceDetectedGithub')
60
+ : sourceType === 'git'
61
+ ? tr('skills.sourceDetectedGit')
62
+ : sourceType === 'agentbuddy'
63
+ ? tr('skills.sourceDetectedAgentbuddy')
64
+ : sourceType === 'local'
65
+ ? tr('skills.sourceDetectedLocal')
66
+ : tr('skills.sourceDetectedUnknown');
67
+ const sourcePreflight = sourceType === 'agentbuddy'
68
+ ? tr('skills.sourcePreflightAgentbuddy')
69
+ : sourceType === 'github' || sourceType === 'git'
70
+ ? tr('skills.sourcePreflightGit')
71
+ : sourceType === 'local'
72
+ ? tr('skills.sourcePreflightLocal')
73
+ : tr('skills.sourcePreflightUnknown');
74
+ const diagnostic = sourceType === 'agentbuddy'
75
+ ? tr('skills.installDiagnosticAgentbuddy')
76
+ : sourceType === 'github' || sourceType === 'git'
77
+ ? tr('skills.installDiagnosticGit')
78
+ : sourceType === 'local'
79
+ ? tr('skills.installDiagnosticLocal')
80
+ : tr('skills.installDiagnosticUnknown');
88
81
  useEffect(() => {
89
82
  const dialog = selectionDialogRef.current;
90
83
  if (!dialog)
@@ -99,7 +92,28 @@ export function SkillsInstallPanel(props) {
99
92
  dialog.close();
100
93
  }
101
94
  }, [props.installSelectionOpen, candidates.length]);
102
- return (_jsxs("article", { className: "bd-card skills-install-panel", children: [props.showTitle === false ? null : _jsx("div", { className: "skills-install-title", children: _jsx("h3", { className: "bd-section-title", children: _jsx(FieldTitle, { help: tr('skills.installInfo'), helpLabel: tr('skills.installInfoLabel'), children: tr('skills.install') }) }) }), _jsxs("div", { className: "skills-install-grid", children: [_jsxs("label", { className: "skills-source-label", children: [_jsx(FieldTitle, { help: (_jsxs("span", { className: "skills-source-help", children: [_jsxs("span", { children: [_jsx("strong", { children: tr('skills.sourceHelpRemoteLabel') }), tr('skills.sourceHelpRemote')] }), _jsxs("span", { children: [_jsx("strong", { children: tr('skills.sourceHelpLocalLabel') }), tr('skills.sourceHelpLocal')] }), _jsxs("span", { children: [_jsx("strong", { children: tr('skills.sourceHelpAgentbuddyLabel') }), tr('skills.sourceHelpAgentbuddy')] })] })), helpLabel: tr('skills.source'), children: tr('skills.source') }), _jsx("div", { className: "skills-source-control", children: _jsx("input", { type: "text", "data-install": "source", "aria-label": tr('skills.source'), placeholder: tr('skills.sourcePlaceholder'), value: props.installSource, onChange: e => props.onInstallSourceChange(e.currentTarget.value) }) })] }), _jsxs("label", { className: "skills-install-field-wide skills-install-path-field", children: [_jsx("span", { children: tr('skills.path') }), _jsx("input", { type: "text", "data-install": "path", placeholder: tr('skills.pathPlaceholder'), value: props.installPath, onChange: e => props.onInstallPathChange(e.currentTarget.value) })] }), _jsxs("label", { className: "skills-install-field-wide skills-install-ref-field", children: [_jsx("span", { children: tr('skills.ref') }), _jsx("input", { type: "text", "data-install": "ref", placeholder: tr('skills.refPlaceholder'), value: props.installRef, onChange: e => props.onInstallRefChange(e.currentTarget.value) })] }), _jsx("div", { className: "skills-install-actions", children: _jsx("button", { type: "button", "data-action": "install", disabled: busy, onClick: () => props.onInstall(), children: props.installDiscovering ? tr('skills.scanning') : props.installBusy ? tr('skills.jobRunning') : tr('skills.installSubmit') }) }), _jsx("div", { className: "skills-install-bottom-row", children: _jsxs("div", { className: "skills-local-discovery-panel", children: [_jsxs("div", { children: [_jsx("strong", { children: tr('skills.localDiscoverTitle') }), _jsx("span", { children: tr('skills.localDiscoverHelp') })] }), _jsx("button", { type: "button", "data-action": "open-native-skill-discovery", onClick: () => props.onOpenNativeDiscovery(), children: tr('skills.localDiscover') })] }) })] }), props.installStatus ? (_jsx("div", { className: "actions skills-install-status-row", children: _jsx("span", { className: statusClass(props.installStatus), "data-skills-status": true, children: props.installStatus.text }) })) : null, _jsx("dialog", { className: "skills-discovery-dialog skills-install-selection-dialog", "data-install-selection-dialog": true, ref: selectionDialogRef, onClose: () => props.onCloseInstallSelection?.(), children: _jsxs("article", { children: [_jsxs("header", { children: [_jsx("h3", { children: tr('skills.installSelectionTitle') }), _jsx("p", { children: tr('skills.installSelectionHelp', { count: candidates.length }) })] }), _jsx("div", { className: "skills-discovery-body skills-install-selection-body", children: _jsxs("div", { className: "skills-candidate-list", "data-install-candidates": true, children: [_jsxs("div", { className: "skills-candidate-list-head", children: [_jsx("span", { children: tr('skills.scanFound', { count: candidates.length }) }), props.onSelectAllInstallSkills ? (_jsx("button", { type: "button", "data-action": "toggle-all-source-skills", disabled: props.installBusy, onClick: () => props.onSelectAllInstallSkills?.(!allSelected), children: allSelected ? tr('skills.discoverClearSelection') : tr('skills.discoverSelectAll') })) : null] }), candidates.map(candidate => (_jsxs("label", { className: "skills-candidate-row", children: [_jsx("input", { type: "checkbox", checked: selectedInstallSkills.has(candidate.name), disabled: props.installBusy, onChange: () => props.onToggleInstallSkill?.(candidate.name) }), _jsxs("span", { children: [_jsx("strong", { children: candidate.name }), _jsxs("small", { children: [candidate.path, candidate.description ? ` · ${candidate.description}` : ''] })] })] }, `${candidate.name}:${candidate.path}`)))] }) }), _jsxs("footer", { className: "actions", children: [_jsx("button", { type: "button", "data-action": "close-install-selection", disabled: props.installBusy, onClick: () => props.onCloseInstallSelection?.(), children: tr('skills.installSelectionCancel') }), _jsx("button", { type: "button", className: "primary", "data-action": "confirm-install-selection", disabled: props.installBusy || selectedInstallSkills.size === 0, onClick: () => props.onConfirmInstallSelection?.(), children: props.installBusy ? tr('skills.jobRunning') : tr('skills.installSelected') })] })] }) })] }));
95
+ useEffect(() => {
96
+ if (!sourceHelpOpen || typeof document === 'undefined')
97
+ return undefined;
98
+ const closeOnOutsidePointer = (event) => {
99
+ if (!sourceHelpRef.current?.contains(event.target))
100
+ setSourceHelpOpen(false);
101
+ };
102
+ const closeOnEscape = (event) => {
103
+ if (event.key === 'Escape')
104
+ setSourceHelpOpen(false);
105
+ };
106
+ document.addEventListener('pointerdown', closeOnOutsidePointer);
107
+ document.addEventListener('keydown', closeOnEscape);
108
+ return () => {
109
+ document.removeEventListener('pointerdown', closeOnOutsidePointer);
110
+ document.removeEventListener('keydown', closeOnEscape);
111
+ };
112
+ }, [sourceHelpOpen]);
113
+ return (_jsxs("article", { className: "bd-card skills-install-panel", children: [props.showTitle === false ? null : _jsx("div", { className: "skills-install-title", children: _jsx("h3", { className: "bd-section-title", children: _jsx(FieldTitle, { help: tr('skills.installInfo'), helpLabel: tr('skills.installInfoLabel'), children: tr('skills.install') }) }) }), props.installTargetSkill ? (_jsxs("div", { className: "skills-inline-target", "data-install-target": props.installTargetSkill, children: [_jsx("span", { children: tr('skills.installTargetHint', { skill: props.installTargetSkill }) }), _jsx("button", { type: "button", "data-action": "clear-install-target", onClick: () => props.onClearInstallTarget?.(), children: tr('skills.clearInstallTarget') })] })) : null, _jsxs("div", { className: "skills-install-grid", children: [_jsxs("div", { className: "skills-source-label", children: [_jsxs("div", { className: "skills-source-heading", ref: sourceHelpRef, onKeyDown: event => {
114
+ if (event.key === 'Escape')
115
+ setSourceHelpOpen(false);
116
+ }, children: [_jsx("label", { htmlFor: sourceInputId, children: tr('skills.source') }), _jsx("button", { type: "button", className: "skills-source-help-trigger", "data-action": "toggle-source-help", "aria-label": tr('skills.sourceHelpOpen'), "aria-expanded": sourceHelpOpen, "aria-controls": sourceHelpId, "aria-haspopup": "dialog", onClick: () => setSourceHelpOpen(open => !open), children: "?" }), sourceHelpOpen ? (_jsxs("aside", { className: "skills-source-help-popover", id: sourceHelpId, role: "dialog", "aria-labelledby": sourceHelpTitleId, "data-source-help-popover": true, children: [_jsxs("div", { className: "skills-source-help-head", children: [_jsxs("div", { children: [_jsx("strong", { id: sourceHelpTitleId, children: tr('skills.sourceHelpTitle') }), _jsx("span", { children: tr('skills.sourceHelpIntro') })] }), _jsx("button", { type: "button", className: "skills-source-help-close", "data-action": "close-source-help", "aria-label": tr('skills.sourceHelpClose'), onClick: () => setSourceHelpOpen(false), children: "\u00D7" })] }), _jsxs("div", { className: "skills-source-help-list", children: [_jsxs("section", { children: [_jsx("strong", { children: tr('skills.sourceHelpRemoteLabel') }), _jsx("span", { children: tr('skills.sourceHelpRemote') }), _jsx("code", { children: tr('skills.sourceHelpRemoteExample') })] }), _jsxs("section", { children: [_jsx("strong", { children: tr('skills.sourceHelpLocalLabel') }), _jsx("span", { children: tr('skills.sourceHelpLocal') }), _jsx("code", { children: tr('skills.sourceHelpLocalExample') })] }), _jsxs("section", { children: [_jsx("strong", { children: tr('skills.sourceHelpAgentbuddyLabel') }), _jsx("span", { children: tr('skills.sourceHelpAgentbuddy') }), _jsx("code", { children: tr('skills.sourceHelpAgentbuddyExample') })] })] })] })) : null] }), _jsx("div", { className: "skills-source-control", children: _jsx("input", { id: sourceInputId, type: "text", "data-install": "source", "aria-label": tr('skills.source'), placeholder: tr('skills.sourcePlaceholder'), value: props.installSource, onChange: e => props.onInstallSourceChange(e.currentTarget.value) }) }), props.installSource.trim() ? (_jsxs("div", { className: `skills-source-feedback is-${sourceType}`, "data-source-hint": sourceType, children: [_jsx("strong", { children: sourceTypeLabel }), _jsx("span", { children: sourcePreflight })] })) : null] }), _jsxs("details", { className: "skills-install-advanced", "data-install-advanced": true, children: [_jsx("summary", { children: tr('skills.advancedOptions') }), _jsxs("div", { className: "skills-install-advanced-body", children: [_jsxs("label", { className: "skills-install-field", children: [_jsx("span", { children: tr('skills.path') }), _jsx("input", { type: "text", "data-install": "path", placeholder: tr('skills.pathPlaceholder'), value: props.installPath, onChange: e => props.onInstallPathChange(e.currentTarget.value) }), _jsx("small", { children: tr('skills.pathHelpInline') })] }), _jsxs("label", { className: "skills-install-field", children: [_jsx("span", { children: tr('skills.ref') }), _jsx("input", { type: "text", "data-install": "ref", placeholder: tr('skills.refPlaceholder'), value: props.installRef, onChange: e => props.onInstallRefChange(e.currentTarget.value) }), _jsx("small", { children: tr('skills.refHelpInline') })] }), _jsxs("label", { className: "skills-scan-toggle", children: [_jsx("input", { type: "checkbox", "data-install": "full-depth", checked: props.installFullDepth, disabled: busy, onChange: event => props.onInstallFullDepthChange(event.currentTarget.checked) }), _jsxs("span", { children: [_jsx("strong", { children: tr('skills.deepScan') }), _jsx("small", { children: tr('skills.deepScanHelp') }), _jsx("small", { className: "muted", children: tr('skills.autoDeepScanHint') })] })] })] })] }), _jsxs("div", { className: "skills-install-actions", children: [_jsx("button", { type: "button", className: "skills-local-discovery-link", "data-action": "open-native-skill-discovery", title: tr('skills.localDiscoverHelp'), onClick: () => props.onOpenNativeDiscovery(), children: tr('skills.localDiscover') }), _jsx("button", { type: "button", className: "primary", "data-action": "install", disabled: busy || !props.installSource.trim(), onClick: () => props.onInstall(), children: props.installDiscovering ? tr('skills.scanning') : props.installBusy ? tr('skills.jobRunning') : tr('skills.installSubmit') })] })] }), props.installStatus ? (_jsx("div", { className: "actions skills-install-status-row", children: _jsx("span", { className: statusClass(props.installStatus), "data-skills-status": true, children: props.installStatus.text }) })) : null, props.installStatus && !props.installStatus.ok ? (_jsxs("div", { className: "skills-install-diagnostic", "data-install-diagnostic": sourceType, role: "alert", children: [_jsx("strong", { children: tr('skills.installDiagnosticTitle') }), _jsx("span", { children: diagnostic })] })) : null, _jsx("dialog", { className: "skills-discovery-dialog skills-install-selection-dialog", "data-install-selection-dialog": true, ref: selectionDialogRef, onClose: () => props.onCloseInstallSelection?.(), children: _jsxs("article", { children: [_jsxs("header", { children: [_jsx("h3", { children: tr('skills.installSelectionTitle') }), _jsx("p", { children: tr('skills.installSelectionHelp', { count: candidates.length }) })] }), _jsx("div", { className: "skills-discovery-body skills-install-selection-body", children: _jsxs("div", { className: "skills-candidate-list", "data-install-candidates": true, children: [_jsxs("div", { className: "skills-candidate-list-head", children: [_jsx("span", { children: tr('skills.scanFound', { count: candidates.length }) }), props.onSelectAllInstallSkills ? (_jsx("button", { type: "button", "data-action": "toggle-all-source-skills", disabled: props.installBusy, onClick: () => props.onSelectAllInstallSkills?.(!allSelected), children: allSelected ? tr('skills.discoverClearSelection') : tr('skills.discoverSelectAll') })) : null] }), candidates.map(candidate => (_jsxs("label", { className: "skills-candidate-row", children: [_jsx("input", { type: "checkbox", checked: selectedInstallSkills.has(candidate.name), disabled: props.installBusy, onChange: () => props.onToggleInstallSkill?.(candidate.name) }), _jsxs("span", { children: [_jsx("strong", { children: candidate.name }), _jsxs("small", { children: [candidate.path, candidate.description ? ` · ${candidate.description}` : ''] })] })] }, `${candidate.name}:${candidate.path}`)))] }) }), _jsxs("footer", { className: "actions", children: [_jsx("button", { type: "button", "data-action": "close-install-selection", disabled: props.installBusy, onClick: () => props.onCloseInstallSelection?.(), children: tr('skills.installSelectionCancel') }), _jsx("button", { type: "button", className: "primary", "data-action": "confirm-install-selection", disabled: props.installBusy || selectedInstallSkills.size === 0, onClick: () => props.onConfirmInstallSelection?.(), children: props.installBusy ? tr('skills.jobRunning') : tr('skills.installSelected') })] })] }) })] }));
103
117
  }
104
118
  export function InstalledSkillsLibrary(props) {
105
119
  const tr = useT();
@@ -111,22 +125,32 @@ export function InstalledSkillsLibrary(props) {
111
125
  const [query, setQuery] = useState('');
112
126
  const [selectionMode, setSelectionMode] = useState(false);
113
127
  const [selected, setSelected] = useState(() => new Set());
128
+ const [filterNames, setFilterNames] = useState(null);
114
129
  const [page, setPage] = useState(0);
115
130
  const [viewportWidth, setViewportWidth] = useState(() => typeof window === 'undefined' ? 1200 : window.innerWidth);
116
131
  const normalizedQuery = query.trim().toLocaleLowerCase();
117
132
  queryRef.current = query;
118
133
  selectionModeRef.current = selectionMode;
119
134
  const filteredSkills = useMemo(() => {
135
+ const base = filterNames ? props.skills.filter(skill => filterNames.has(skill.name)) : props.skills;
120
136
  if (!normalizedQuery)
121
- return props.skills;
122
- return props.skills.filter(skill => [
137
+ return base;
138
+ return base.filter(skill => [
123
139
  skill.name,
124
140
  skill.displayName,
125
141
  skill.description,
126
142
  ...(skill.tags ?? []),
127
143
  sourceLabel(skill, tr),
128
144
  ].filter(Boolean).join('\n').toLocaleLowerCase().includes(normalizedQuery));
129
- }, [normalizedQuery, props.skills, tr]);
145
+ }, [filterNames, normalizedQuery, props.skills, tr]);
146
+ // Filter names that aren't installed (typically dangling refs from the
147
+ // overview badge): rendered as actionable install rows, not silently hidden.
148
+ const missingFiltered = useMemo(() => {
149
+ if (!filterNames)
150
+ return [];
151
+ const installed = new Set(props.skills.map(skill => skill.name));
152
+ return [...filterNames].filter(name => !installed.has(name)).sort();
153
+ }, [filterNames, props.skills]);
130
154
  const pageSize = installedSkillsColumnCount(viewportWidth) * INSTALLED_SKILLS_ROWS_PER_PAGE;
131
155
  const pageCount = Math.max(1, Math.ceil(filteredSkills.length / pageSize));
132
156
  const visibleSkills = filteredSkills.slice(page * pageSize, page * pageSize + pageSize);
@@ -141,7 +165,18 @@ export function InstalledSkillsLibrary(props) {
141
165
  window.addEventListener('resize', onResize);
142
166
  return () => window.removeEventListener('resize', onResize);
143
167
  }, []);
144
- useEffect(() => setPage(0), [normalizedQuery, pageSize]);
168
+ // Cross-tab arrival: seed the search box / name filter once per incoming
169
+ // intent. The parent consumes the intent right after, so neither re-applies.
170
+ useEffect(() => {
171
+ if (props.externalQuery != null && props.externalQuery !== '')
172
+ setQuery(props.externalQuery);
173
+ }, [props.externalQuery]);
174
+ useEffect(() => {
175
+ if (props.externalFilterNames && props.externalFilterNames.length > 0) {
176
+ setFilterNames(new Set(props.externalFilterNames));
177
+ }
178
+ }, [props.externalFilterNames]);
179
+ useEffect(() => setPage(0), [normalizedQuery, pageSize, filterNames]);
145
180
  useEffect(() => {
146
181
  setPage(current => Math.min(Math.max(0, current), pageCount - 1));
147
182
  }, [pageCount]);
@@ -236,10 +271,21 @@ export function InstalledSkillsLibrary(props) {
236
271
  cancelSelection(true);
237
272
  } }), query ? (_jsx("button", { type: "button", "data-action": "clear-installed-search", "aria-label": tr('skills.clearSearch'), onClick: () => setQuery(''), children: "\u00D7" })) : null] }), selectionMode ? (_jsxs("label", { className: "skills-select-all-results", children: [_jsx("input", { ref: selectAllRef, type: "checkbox", checked: allResultsSelected, disabled: filteredSkills.length === 0 || props.removingNames.size > 0, onChange: toggleAllResults }), _jsx("span", { children: normalizedQuery
238
273
  ? tr('skills.selectSearchResults', { count: filteredSkills.length })
239
- : tr('skills.selectAllSkills', { count: filteredSkills.length }) })] })) : null, normalizedQuery ? _jsx("span", { className: "skills-filter-count", children: tr('skills.resultCount', { count: filteredSkills.length }) }) : null, pageCount > 1 ? (_jsxs("div", { className: "skills-pager", children: [_jsx("button", { type: "button", className: "skills-pager-button", "data-action": "page-installed-skills", "data-dir": "-1", "aria-label": tr('skills.prevPage'), title: tr('skills.prevPage'), disabled: page === 0, onClick: () => setPage(current => Math.max(0, current - 1)), children: "\u2039" }), _jsx("span", { children: tr('skills.pageStatus', { page: page + 1, pages: pageCount }) }), _jsx("button", { type: "button", className: "skills-pager-button", "data-action": "page-installed-skills", "data-dir": "1", "aria-label": tr('skills.nextPage'), title: tr('skills.nextPage'), disabled: page >= pageCount - 1, onClick: () => setPage(current => Math.min(pageCount - 1, current + 1)), children: "\u203A" })] })) : null] }), _jsx("div", { className: "skills-installed-live", role: "status", "aria-live": "polite", children: props.status ? _jsx("span", { className: statusClass(props.status), children: props.status.text }) : null }), props.skills.length === 0 ? (_jsxs("div", { className: "skills-library-empty", children: [_jsx("strong", { children: tr('skills.emptyTitle') }), _jsx("p", { children: tr('skills.emptyHelp') })] })) : filteredSkills.length === 0 ? (_jsxs("div", { className: "skills-library-empty", "data-empty": "search", children: [_jsx("strong", { children: tr('skills.noResultsTitle', { query: query.trim() }) }), _jsx("p", { children: tr('skills.noResultsHelp') }), _jsx("button", { type: "button", "data-action": "clear-installed-search-empty", onClick: () => setQuery(''), children: tr('skills.clearSearch') })] })) : (_jsx("div", { className: "skills-list", children: visibleSkills.map(skill => {
274
+ : tr('skills.selectAllSkills', { count: filteredSkills.length }) })] })) : null, filterNames ? (_jsxs("button", { type: "button", className: "skills-library-filter-chip", "data-action": "clear-library-filter", title: [...filterNames].join(', '), "aria-label": tr('skills.clearFilter'), onClick: () => setFilterNames(null), children: [tr('skills.libraryFilterActive', { count: filterNames.size }), " \u00D7"] })) : null, normalizedQuery ? _jsx("span", { className: "skills-filter-count", children: tr('skills.resultCount', { count: filteredSkills.length }) }) : null, pageCount > 1 ? (_jsxs("div", { className: "skills-pager", children: [_jsx("button", { type: "button", className: "skills-pager-button", "data-action": "page-installed-skills", "data-dir": "-1", "aria-label": tr('skills.prevPage'), title: tr('skills.prevPage'), disabled: page === 0, onClick: () => setPage(current => Math.max(0, current - 1)), children: "\u2039" }), _jsx("span", { children: tr('skills.pageStatus', { page: page + 1, pages: pageCount }) }), _jsx("button", { type: "button", className: "skills-pager-button", "data-action": "page-installed-skills", "data-dir": "1", "aria-label": tr('skills.nextPage'), title: tr('skills.nextPage'), disabled: page >= pageCount - 1, onClick: () => setPage(current => Math.min(pageCount - 1, current + 1)), children: "\u203A" })] })) : null] }), _jsx("div", { className: "skills-installed-live", role: "status", "aria-live": "polite", children: props.status ? _jsx("span", { className: statusClass(props.status), children: props.status.text }) : null }), missingFiltered.length > 0 && (_jsxs("div", { className: "skills-missing-list", "data-missing-skills": true, children: [_jsx("strong", { children: tr('skills.missingFilteredTitle') }), missingFiltered.map(name => (_jsxs("div", { className: "skills-missing-row", "data-missing-skill": name, children: [_jsx("span", { className: "skills-missing-row-name", children: name }), _jsx("small", { className: "muted", children: tr('skills.dangling') }), _jsx("button", { type: "button", "data-action": "install-dangling-skill", disabled: !props.onInstallMissing, onClick: () => props.onInstallMissing?.(name), children: tr('skills.installNow') })] }, name)))] })), props.skills.length === 0 ? (_jsxs("div", { className: "skills-library-empty", children: [_jsx("strong", { children: tr('skills.emptyTitle') }), _jsx("p", { children: tr('skills.emptyHelp') })] })) : filteredSkills.length === 0 && missingFiltered.length > 0 ? null : filteredSkills.length === 0 ? (_jsxs("div", { className: "skills-library-empty", "data-empty": "search", children: [_jsx("strong", { children: tr('skills.noResultsTitle', { query: query.trim() }) }), _jsx("p", { children: tr('skills.noResultsHelp') }), _jsx("button", { type: "button", "data-action": "clear-installed-search-empty", onClick: () => setQuery(''), children: tr('skills.clearSearch') })] })) : (_jsx("div", { className: "skills-list", children: visibleSkills.map(skill => {
240
275
  const isSelected = selected.has(skill.name);
241
276
  const isRemoving = props.removingNames.has(skill.name);
242
- return (_jsxs("article", { className: `skills-row skills-installed-card${selectionMode ? ' is-selectable' : ''}${isSelected ? ' is-selected' : ''}${isRemoving ? ' is-removing' : ''}`, "data-skill": skill.name, "data-selection-mode": selectionMode ? 'true' : undefined, "aria-busy": isRemoving || undefined, onClick: selectionMode ? () => toggleSelected(skill.name) : undefined, children: [_jsxs("div", { className: "skills-row-body", children: [_jsxs("div", { className: "skills-row-title", children: [selectionMode ? (_jsx("input", { type: "checkbox", "aria-label": tr('skills.selectNamed', { skill: skill.displayName ?? skill.name }), checked: isSelected, disabled: isRemoving, onClick: event => event.stopPropagation(), onChange: () => toggleSelected(skill.name) })) : null, _jsxs("span", { children: [_jsx("strong", { children: skill.displayName ?? skill.name }), skill.displayName && skill.displayName !== skill.name ? _jsx("small", { className: "skills-canonical-name", children: skill.name }) : null] })] }), skill.description ? _jsx("p", { children: skill.description }) : null, _jsx("small", { className: "skills-source-badge", children: sourceLabel(skill, tr) }), isRemoving ? _jsx("span", { className: "skills-removing-label", children: tr('skills.removing') }) : null] }), selectionMode ? null : (_jsxs("div", { className: "skills-card-actions", children: [_jsx("button", { type: "button", "data-action": "update-skill", disabled: props.busySkill === `${skill.name}:update` || isRemoving, onClick: () => props.onUpdate(skill.name), children: tr('skills.update') }), _jsx("button", { type: "button", "data-action": "remove-skill", disabled: isRemoving, onClick: () => props.onRequestRemove([skill.name]), children: tr('skills.remove') })] }))] }, skill.name));
277
+ return (_jsxs("article", { className: `skills-row skills-installed-card${selectionMode ? ' is-selectable' : ''}${isSelected ? ' is-selected' : ''}${isRemoving ? ' is-removing' : ''}`, "data-skill": skill.name, "data-selection-mode": selectionMode ? 'true' : undefined, "aria-busy": isRemoving || undefined, onClick: selectionMode ? () => toggleSelected(skill.name) : undefined, children: [_jsxs("div", { className: "skills-row-body", children: [_jsxs("div", { className: "skills-row-title", children: [selectionMode ? (_jsx("input", { type: "checkbox", "aria-label": tr('skills.selectNamed', { skill: skill.displayName ?? skill.name }), checked: isSelected, disabled: isRemoving, onClick: event => event.stopPropagation(), onChange: () => toggleSelected(skill.name) })) : null, _jsxs("span", { children: [_jsx("strong", { children: skill.displayName ?? skill.name }), skill.displayName && skill.displayName !== skill.name ? _jsx("small", { className: "skills-canonical-name", children: skill.name }) : null] })] }), skill.description ? _jsx("p", { children: skill.description }) : null, _jsx("small", { className: "skills-source-badge", children: sourceLabel(skill, tr) }), props.graph ? (() => {
278
+ const node = props.graph.skills.get(skill.name);
279
+ const packsUnknown = props.packsKnown === false;
280
+ const packCount = node?.packIds.length ?? 0;
281
+ const botCount = node ? new Set([...node.directBotIds, ...node.viaPackBotIds]).size : 0;
282
+ const packTitle = packsUnknown
283
+ ? tr('skills.healthUnknown')
284
+ : (node?.packIds ?? [])
285
+ .map(id => props.packNames?.find(pack => pack.id === id)?.name ?? id)
286
+ .join(', ');
287
+ return (_jsxs("span", { className: "skills-usage-chips", children: [_jsx("button", { type: "button", className: "skills-usage-chip", "data-action": "show-skill-packs", disabled: packsUnknown || packCount === 0 || !props.onShowSkillPacks, title: packTitle || undefined, onClick: event => { event.stopPropagation(); props.onShowSkillPacks?.(skill.name); }, children: packsUnknown ? tr('skills.usedInPacksUnknown') : tr('skills.usedInPacks', { count: packCount }) }), _jsx("button", { type: "button", className: "skills-usage-chip", "data-action": "show-skill-bots", disabled: botCount === 0 || !props.onShowSkillBots, onClick: event => { event.stopPropagation(); props.onShowSkillBots?.(skill.name); }, children: tr('skills.usedByBots', { count: botCount }) })] }));
288
+ })() : null, isRemoving ? _jsx("span", { className: "skills-removing-label", children: tr('skills.removing') }) : null] }), selectionMode ? null : (_jsxs("div", { className: "skills-card-actions", children: [_jsx("button", { type: "button", "data-action": "update-skill", disabled: props.busySkill === `${skill.name}:update` || isRemoving, onClick: () => props.onUpdate(skill.name), children: tr('skills.update') }), _jsx("button", { type: "button", "data-action": "remove-skill", disabled: isRemoving, onClick: () => props.onRequestRemove([skill.name]), children: tr('skills.remove') })] }))] }, skill.name));
243
289
  }) }))] }), selectionMode ? (_jsxs("div", { className: "skills-bulk-action-bar", role: "toolbar", "aria-label": tr('skills.bulkActions'), children: [_jsxs("span", { children: [_jsx("strong", { children: tr('skills.selectedCount', { count: selected.size }) }), hiddenSelectedCount > 0 ? _jsx("small", { children: tr('skills.selectedHidden', { count: hiddenSelectedCount }) }) : null] }), _jsx("button", { type: "button", className: "danger", "data-action": "remove-selected-skills", disabled: selected.size === 0 || props.removingNames.size > 0, onClick: () => props.onRequestRemove([...selected]), children: tr('skills.removeSelected', { count: selected.size }) })] })) : null] }));
244
290
  }
245
291
  export function RemoveSkillsDialog(props) {
@@ -249,6 +295,7 @@ export function RemoveSkillsDialog(props) {
249
295
  const force = props.references.length > 0;
250
296
  const visibleNames = names.slice(0, 3);
251
297
  const referencedBotCount = new Set(props.references.flatMap(reference => reference.bots)).size;
298
+ const referencedPackCount = new Set(props.references.flatMap(reference => reference.packs)).size;
252
299
  useEffect(() => {
253
300
  const dialog = dialogRef.current;
254
301
  if (!dialog)
@@ -278,11 +325,15 @@ export function RemoveSkillsDialog(props) {
278
325
  ? tr('skills.removeOneTitle', { skill: names[0] })
279
326
  : tr('skills.removeManyTitle', { count: names.length }) }), _jsx("p", { children: force
280
327
  ? names.length === 1
281
- ? tr('skills.removeInUseOneHelp', { count: referencedBotCount })
282
- : tr('skills.removeInUseManyHelp', { count: referencedBotCount })
328
+ ? referencedPackCount > 0
329
+ ? tr('skills.removeInUseOneHelpWithPacks', { botCount: referencedBotCount, packCount: referencedPackCount })
330
+ : tr('skills.removeInUseOneHelp', { count: referencedBotCount })
331
+ : referencedPackCount > 0
332
+ ? tr('skills.removeInUseManyHelpWithPacks', { botCount: referencedBotCount, packCount: referencedPackCount })
333
+ : tr('skills.removeInUseManyHelp', { count: referencedBotCount })
283
334
  : names.length === 1
284
335
  ? tr('skills.removeOnePermanentHelp')
285
- : tr('skills.removePermanentHelp') })] }), _jsxs("div", { className: "skills-remove-dialog-body", children: [names.length > 1 ? (_jsxs(_Fragment, { children: [_jsx("ul", { children: visibleNames.map(name => _jsx("li", { children: name }, name)) }), names.length > visibleNames.length ? _jsx("p", { children: tr('skills.removeMore', { count: names.length - visibleNames.length }) }) : null] })) : null, props.references.length > 0 ? (_jsxs("div", { className: "skills-remove-reference-warning", role: "alert", children: [_jsx("strong", { children: tr('skills.removeReferencesDetail') }), _jsx("ul", { children: props.references.map(reference => (_jsxs("li", { children: [_jsx("strong", { children: reference.name }), _jsx("span", { children: reference.bots.join(', ') })] }, reference.name))) })] })) : null, props.error ? _jsx("p", { className: "hint-warn", role: "alert", children: props.error }) : null] }), _jsxs("footer", { className: "actions", children: [_jsx("button", { type: "button", autoFocus: true, disabled: props.busy, onClick: props.onCancel, children: tr('skills.cancel') }), _jsx("button", { type: "button", className: "danger", disabled: props.busy, onClick: () => props.onConfirm(force), children: props.busy
336
+ : tr('skills.removePermanentHelp') })] }), _jsxs("div", { className: "skills-remove-dialog-body", children: [names.length > 1 ? (_jsxs(_Fragment, { children: [_jsx("ul", { children: visibleNames.map(name => _jsx("li", { children: name }, name)) }), names.length > visibleNames.length ? _jsx("p", { children: tr('skills.removeMore', { count: names.length - visibleNames.length }) }) : null] })) : null, props.references.length > 0 ? (_jsxs("div", { className: "skills-remove-reference-warning", role: "alert", children: [_jsx("strong", { children: tr('skills.removeReferencesDetail') }), _jsx("ul", { children: props.references.map(reference => (_jsxs("li", { children: [_jsx("strong", { children: reference.name }), _jsxs("span", { children: [reference.bots.join(', '), reference.bots.length > 0 && reference.packs.length > 0 ? ' · ' : '', reference.packs.length > 0 ? `${tr('skills.packChips')}: ${reference.packs.join(', ')}` : ''] })] }, reference.name))) })] })) : null, props.error ? _jsx("p", { className: "hint-warn", role: "alert", children: props.error }) : null] }), _jsxs("footer", { className: "actions", children: [_jsx("button", { type: "button", autoFocus: true, disabled: props.busy, onClick: props.onCancel, children: tr('skills.cancel') }), _jsx("button", { type: "button", className: "danger", disabled: props.busy, onClick: () => props.onConfirm(force), children: props.busy
286
337
  ? tr('skills.removing')
287
338
  : force
288
339
  ? tr('skills.removeAnyway')
@@ -290,18 +341,29 @@ export function RemoveSkillsDialog(props) {
290
341
  ? tr('skills.removeOneConfirm')
291
342
  : tr('skills.removeSelected', { count: names.length }) })] })] }) }));
292
343
  }
293
- function SkillsPage() {
344
+ /** Exported for tests (cross-tab navigation integration). Mounted in the app
345
+ * via renderSkillsPage below. */
346
+ export function SkillsPage() {
294
347
  const tr = useT();
295
348
  const mountedRef = useRef(true);
296
349
  const timersRef = useRef(new Set());
297
350
  const discoveryDialogRef = useRef(null);
298
- const [skills, setSkills] = useState([]);
299
- const [nativeSkillGroups, setNativeSkillGroups] = useState([]);
300
- const [bots, setBots] = useState([]);
301
- const [trustProjectSkills, setTrustProjectSkills] = useState('off');
302
- const [delivery, setDelivery] = useState('auto');
303
- const [loadError, setLoadError] = useState(null);
304
- const [loading, setLoading] = useState(true);
351
+ const { skills, nativeSkillGroups, bots, packs, trustProjectSkills, delivery, loading, loadError, packsError, packsKnown, refresh, setSkills, setBots, setTrustProjectSkills, setDelivery, } = useSkillsData({ apiUnavailableText: tr('skills.apiUnavailable') });
352
+ const [activeTab, setActiveTab] = useState('library');
353
+ // Cross-tab navigation: a chip click in one table jumps to another tab with
354
+ // context (search prefill / focused entity). The target tab consumes the
355
+ // intent once so it doesn't re-apply on later visits.
356
+ const [navIntent, setNavIntent] = useState(null);
357
+ // Sticky install target: when a missing skill sends the user to the install
358
+ // wizard, the discovery result preselects just that skill. Prefill only —
359
+ // installing still requires the user to point at a source and confirm.
360
+ const [installTargetSkill, setInstallTargetSkill] = useState(null);
361
+ const navigateTo = useCallback((intent) => {
362
+ setNavIntent(intent);
363
+ setInstallTargetSkill(intent.installTargetSkill ?? null);
364
+ setActiveTab(intent.tab);
365
+ }, []);
366
+ const consumeNavIntent = useCallback(() => setNavIntent(null), []);
305
367
  const [installSource, setInstallSource] = useState('');
306
368
  const [installPath, setInstallPath] = useState('');
307
369
  const [installRef, setInstallRef] = useState('');
@@ -310,6 +372,10 @@ function SkillsPage() {
310
372
  const [installDiscovering, setInstallDiscovering] = useState(false);
311
373
  const [installSelectionOpen, setInstallSelectionOpen] = useState(false);
312
374
  const [installCandidates, setInstallCandidates] = useState([]);
375
+ /** operator explicitly asked to skip the shallow pass */
376
+ const [installForceFullDepth, setInstallForceFullDepth] = useState(false);
377
+ /** discovery had to fall back to a full-depth scan to find anything */
378
+ const [installDeepScanned, setInstallDeepScanned] = useState(false);
313
379
  const [selectedInstallSkills, setSelectedInstallSkills] = useState(() => new Set());
314
380
  const [discoveryOpen, setDiscoveryOpen] = useState(false);
315
381
  const [discoveryBusy, setDiscoveryBusy] = useState(false);
@@ -317,7 +383,7 @@ function SkillsPage() {
317
383
  const [selectedDiscovered, setSelectedDiscovered] = useState(() => new Set());
318
384
  const [globalBusy, setGlobalBusy] = useState(null);
319
385
  const [skillBusy, setSkillBusy] = useState(null);
320
- const [botBusy, setBotBusy] = useState(null);
386
+ const [busyBotIds, setBusyBotIds] = useState(() => new Set());
321
387
  const [botStatuses, setBotStatuses] = useState({});
322
388
  const [installedStatus, setInstalledStatus] = useState(null);
323
389
  const [pendingRemoval, setPendingRemoval] = useState(null);
@@ -325,6 +391,11 @@ function SkillsPage() {
325
391
  const [removalReferences, setRemovalReferences] = useState([]);
326
392
  const [removalError, setRemovalError] = useState(null);
327
393
  const [removingNames, setRemovingNames] = useState(() => new Set());
394
+ const botsRef = useRef(bots);
395
+ const botAssignmentQueuesRef = useRef(new Map());
396
+ useEffect(() => {
397
+ botsRef.current = bots;
398
+ }, [bots]);
328
399
  const installedNames = useMemo(() => new Set(skills.map(skill => skill.name)), [skills]);
329
400
  const activeDiscoveryGroup = useMemo(() => {
330
401
  if (nativeSkillGroups.length === 0)
@@ -354,70 +425,29 @@ function SkillsPage() {
354
425
  }, ms);
355
426
  timersRef.current.add(id);
356
427
  }), []);
357
- const fetchData = useCallback(async () => {
358
- const [skillsRes, botsRes] = await Promise.all([
359
- fetch('/api/skills'),
360
- fetch('/api/bots'),
361
- ]);
362
- const skillsBody = await skillsRes.json().catch(() => ({}));
363
- const botsBody = await botsRes.json().catch(() => ({}));
364
- if (!skillsRes.ok) {
365
- const error = skillsBody?.error ?? `skills HTTP ${skillsRes.status}`;
366
- throw new Error(error === 'not_found_yet' || error === 'not_found' ? tr('skills.apiUnavailable') : error);
367
- }
368
- if (!botsRes.ok)
369
- throw new Error(botsBody?.error ?? `bots HTTP ${botsRes.status}`);
370
- return {
371
- skills: Array.isArray(skillsBody.skills) ? skillsBody.skills : [],
372
- nativeSkillGroups: Array.isArray(skillsBody.nativeSkillGroups) ? skillsBody.nativeSkillGroups : [],
373
- bots: Array.isArray(botsBody.bots) ? botsBody.bots : [],
374
- trustProjectSkills: skillsBody.trustProjectSkills === 'all' ? 'all' : 'off',
375
- delivery: (skillsBody.delivery === 'prompt' || skillsBody.delivery === 'native' ? skillsBody.delivery : 'auto'),
376
- };
377
- }, [tr]);
378
- const refresh = useCallback(async () => {
379
- setLoading(true);
380
- try {
381
- const next = await fetchData();
382
- if (!mountedRef.current)
383
- return;
384
- setSkills(next.skills);
385
- setNativeSkillGroups(next.nativeSkillGroups);
386
- setBots(next.bots);
387
- setTrustProjectSkills(next.trustProjectSkills);
388
- setDelivery(next.delivery);
389
- setLoadError(null);
390
- setSelectedDiscovered(selected => {
391
- const valid = new Set();
392
- const installed = new Set(next.skills.map(skill => skill.name));
393
- for (const group of next.nativeSkillGroups) {
394
- for (const skill of group.skills) {
395
- const path = skill.rootDir ?? skill.source?.root ?? '';
396
- if (path && !installed.has(skill.name) && selected.has(path))
397
- valid.add(path);
398
- }
399
- }
400
- return valid;
401
- });
402
- }
403
- catch (err) {
404
- if (!mountedRef.current)
405
- return;
406
- setLoadError(err?.message ?? String(err));
407
- }
408
- finally {
409
- if (mountedRef.current)
410
- setLoading(false);
411
- }
412
- }, [fetchData]);
413
428
  useEffect(() => {
414
429
  mountedRef.current = true;
415
- void refresh();
416
430
  return () => {
417
431
  mountedRef.current = false;
418
432
  clearTimers();
419
433
  };
420
- }, [clearTimers, refresh]);
434
+ }, [clearTimers]);
435
+ // Drop native-discovery selections that became invalid after a refresh
436
+ // (skill got installed, or its group disappeared).
437
+ useEffect(() => {
438
+ setSelectedDiscovered(selected => {
439
+ const valid = new Set();
440
+ const installed = new Set(skills.map(skill => skill.name));
441
+ for (const group of nativeSkillGroups) {
442
+ for (const skill of group.skills) {
443
+ const path = skill.rootDir ?? skill.source?.root ?? '';
444
+ if (path && !installed.has(skill.name) && selected.has(path))
445
+ valid.add(path);
446
+ }
447
+ }
448
+ return valid.size === selected.size ? selected : valid;
449
+ });
450
+ }, [skills, nativeSkillGroups]);
421
451
  useEffect(() => {
422
452
  if (!installedStatus || removingNames.size > 0)
423
453
  return undefined;
@@ -446,22 +476,22 @@ function SkillsPage() {
446
476
  setStatus({ text: tr('skills.jobRunning'), ok: true });
447
477
  for (;;) {
448
478
  if (!mountedRef.current)
449
- return;
479
+ return null;
450
480
  if (current.status === 'succeeded') {
451
481
  setStatus({ text: tr('skills.saved'), ok: true });
452
482
  if (refreshOnSuccess)
453
483
  await refresh();
454
- return;
484
+ return current;
455
485
  }
456
486
  if (current.status === 'failed') {
457
487
  throw new Error(current.error ?? 'job_failed');
458
488
  }
459
489
  await delay(800);
460
490
  if (!mountedRef.current)
461
- return;
491
+ return null;
462
492
  const body = await jsonRequest(`/api/skills/jobs/${encodeURIComponent(current.id)}`, { method: 'GET' });
463
493
  if (!mountedRef.current)
464
- return;
494
+ return null;
465
495
  current = body.job;
466
496
  }
467
497
  }
@@ -474,19 +504,30 @@ function SkillsPage() {
474
504
  setInstallCandidates([]);
475
505
  setSelectedInstallSkills(new Set());
476
506
  setInstallSelectionOpen(false);
507
+ setInstallDeepScanned(false);
477
508
  }
478
509
  function sourceRequestBody() {
510
+ // The source string is sent verbatim: parseSkillInstallSource() on the
511
+ // daemon is the single authority for classification (GitHub shorthand,
512
+ // git remotes, agentbuddy identifiers/commands, local paths). Rewriting it
513
+ // here previously corrupted sources — `git+` was prepended to every
514
+ // non-github http(s) URL, and client-side guessing diverged from the CLI.
515
+ const source = installSource.trim();
479
516
  return {
480
- source: installSource.trim(),
517
+ source,
481
518
  path: installPath.trim() || undefined,
482
519
  ref: installRef.trim() || undefined,
520
+ ...(installForceFullDepth ? { fullDepth: true } : {}),
483
521
  };
484
522
  }
485
- function installRequestBody(skillNames) {
523
+ function installRequestBody(skillNames, fullDepth = installForceFullDepth || installDeepScanned) {
486
524
  const selected = skillNames ?? [...selectedInstallSkills];
487
525
  return {
488
526
  ...sourceRequestBody(),
489
527
  skillNames: selected.length > 0 ? selected : undefined,
528
+ // If the candidates only surfaced under the recursive scan, the install
529
+ // must use the same depth or it won't find them again.
530
+ ...(fullDepth ? { fullDepth: true } : {}),
490
531
  };
491
532
  }
492
533
  async function discoverInstallCandidates() {
@@ -498,12 +539,17 @@ function SkillsPage() {
498
539
  body: JSON.stringify(sourceRequestBody()),
499
540
  });
500
541
  if (!mountedRef.current)
501
- return { skills: [], directInstall: false };
542
+ return { skills: [], directInstall: false, fullDepth: installForceFullDepth };
502
543
  const directInstall = body.discovery?.directInstall === true;
503
544
  const skills = Array.isArray(body.discovery?.skills) ? body.discovery.skills : [];
545
+ const deepScanned = body.discovery?.deepScanned === true;
546
+ setInstallDeepScanned(deepScanned);
504
547
  setInstallCandidates(skills);
505
- setSelectedInstallSkills(new Set(skills.map(skill => skill.name)));
506
- return { skills, directInstall };
548
+ // Target semantics: with an install target, only the matching candidate
549
+ // is preselected; if the target isn't in this source, select nothing so
550
+ // a wrong repo can't be accidentally bulk-confirmed.
551
+ setSelectedInstallSkills(selectInstallCandidates(skills, installTargetSkill).selected);
552
+ return { skills, directInstall, fullDepth: installForceFullDepth || deepScanned };
507
553
  }
508
554
  finally {
509
555
  if (mountedRef.current)
@@ -536,22 +582,29 @@ function SkillsPage() {
536
582
  return tr('skills.agentbuddyInvalid');
537
583
  return msg;
538
584
  }
539
- async function submitSkillInstall(skillNames) {
585
+ async function submitSkillInstall(skillNames, fullDepth = installForceFullDepth || installDeepScanned) {
540
586
  setInstallBusy(true);
541
587
  try {
542
588
  const body = await jsonRequest('/api/skills/install', {
543
589
  method: 'POST',
544
- body: JSON.stringify(installRequestBody(skillNames)),
590
+ body: JSON.stringify(installRequestBody(skillNames, fullDepth)),
545
591
  });
546
592
  if (!mountedRef.current)
547
- return;
548
- await waitForSkillJob(body.job, setInstallStatus);
549
- if (mountedRef.current)
550
- clearInstallDiscovery();
593
+ return null;
594
+ const completed = await waitForSkillJob(body.job, setInstallStatus);
595
+ if (!mountedRef.current || !completed)
596
+ return null;
597
+ const installed = (completed.skills ?? (completed.skill ? [completed.skill] : []))
598
+ .map(skill => skill.name)
599
+ .filter(Boolean);
600
+ clearInstallDiscovery();
601
+ setInstallTargetSkill(null);
602
+ return [...new Set(installed.length > 0 ? installed : (skillNames ?? []))];
551
603
  }
552
604
  catch (err) {
553
605
  if (mountedRef.current)
554
606
  setInstallStatus({ text: `${tr('skills.failed')}: ${mapInstallError(err?.message ?? String(err))}`, ok: false });
607
+ return null;
555
608
  }
556
609
  finally {
557
610
  if (mountedRef.current)
@@ -574,44 +627,65 @@ function SkillsPage() {
574
627
  async function installSkill() {
575
628
  if (!installSource.trim()) {
576
629
  setInstallStatus({ text: tr('skills.sourceRequired'), ok: false });
577
- return;
630
+ return null;
578
631
  }
579
632
  try {
580
633
  setInstallSelectionOpen(false);
581
- const { skills, directInstall } = await discoverInstallCandidates();
634
+ const { skills, directInstall, fullDepth } = await discoverInstallCandidates();
582
635
  if (!mountedRef.current)
583
- return;
636
+ return null;
584
637
  // agentbuddy sources — a pasted `agentbuddy:<id>` OR a marketplace URL the
585
638
  // backend recognized — resolve their own skill set, so install directly
586
639
  // (no candidate selection). The server decides this so the client needn't
587
640
  // know the identifier prefix or the configured marketplace hosts.
588
641
  if (directInstall) {
589
- await submitSkillInstall();
590
- return;
642
+ return await submitSkillInstall(undefined, fullDepth);
591
643
  }
592
644
  if (skills.length === 0) {
593
645
  setInstallStatus({ text: tr('skills.scanEmpty'), ok: false });
594
- return;
646
+ return null;
595
647
  }
596
- if (skills.length === 1) {
597
- await submitSkillInstall([skills[0].name]);
598
- return;
648
+ const { targetMissing } = selectInstallCandidates(skills, installTargetSkill);
649
+ // Single-candidate fast path only when it doesn't contradict the target:
650
+ // if the user came to install a specific skill and this source doesn't
651
+ // have it, fall through to the (empty) selection + warning instead.
652
+ if (skills.length === 1 && !targetMissing) {
653
+ return await submitSkillInstall([skills[0].name], fullDepth);
599
654
  }
600
- setInstallStatus({ text: tr('skills.scanFound', { count: skills.length }), ok: true });
655
+ setInstallStatus(targetMissing
656
+ ? { text: tr('skills.installTargetNotFound', { skill: installTargetSkill }), ok: false }
657
+ : { text: tr('skills.scanFound', { count: skills.length }), ok: true });
601
658
  setInstallSelectionOpen(true);
659
+ return null;
602
660
  }
603
661
  catch (err) {
604
662
  if (mountedRef.current)
605
663
  setInstallStatus({ text: `${tr('skills.failed')}: ${mapInstallError(err?.message ?? String(err))}`, ok: false });
664
+ return null;
606
665
  }
607
666
  }
608
667
  async function confirmInstallSelection() {
609
668
  const selected = [...selectedInstallSkills];
610
669
  if (selected.length === 0) {
611
670
  setInstallStatus({ text: tr('skills.discoverNothingSelected'), ok: false });
612
- return;
671
+ return null;
613
672
  }
614
- await submitSkillInstall(selected);
673
+ return await submitSkillInstall(selected);
674
+ }
675
+ async function createPackFromInstalledSkills(input) {
676
+ await jsonRequest('/api/skill-packs', {
677
+ method: 'POST',
678
+ body: JSON.stringify({
679
+ id: input.id,
680
+ name: input.name,
681
+ include: input.skillNames.map(skillName => `skill:${skillName}`),
682
+ }),
683
+ });
684
+ if (!mountedRef.current)
685
+ return;
686
+ await refresh();
687
+ if (mountedRef.current)
688
+ setActiveTab('packs');
615
689
  }
616
690
  async function registerDiscoveredSkills() {
617
691
  const selected = [...selectedDiscovered].filter(Boolean);
@@ -757,7 +831,10 @@ function SkillsPage() {
757
831
  const bots = Array.isArray(item?.affectedBots)
758
832
  ? item.affectedBots.map((bot) => bot?.botName || bot?.larkAppId || '').filter(Boolean)
759
833
  : referencingBotLabels(name);
760
- return name ? { name, bots } : null;
834
+ const packs = Array.isArray(item?.affectedPacks)
835
+ ? item.affectedPacks.filter((pack) => typeof pack === 'string')
836
+ : [];
837
+ return name ? { name, bots, packs } : null;
761
838
  }).filter((item) => item !== null)
762
839
  : [];
763
840
  setRemovalReferences(references);
@@ -775,21 +852,44 @@ function SkillsPage() {
775
852
  setRemovingNames(new Set());
776
853
  }
777
854
  }
778
- async function setBotSkills(appId, names) {
779
- const busyKey = `${appId}:set`;
780
- setBotBusy(busyKey);
855
+ function enqueueBotAssignment(appId, operation) {
856
+ const previous = botAssignmentQueuesRef.current.get(appId) ?? Promise.resolve();
857
+ setBusyBotIds(current => new Set(current).add(appId));
858
+ const task = previous.catch(() => undefined).then(operation);
859
+ botAssignmentQueuesRef.current.set(appId, task);
860
+ return task.finally(() => {
861
+ if (botAssignmentQueuesRef.current.get(appId) !== task)
862
+ return;
863
+ botAssignmentQueuesRef.current.delete(appId);
864
+ if (!mountedRef.current)
865
+ return;
866
+ setBusyBotIds(current => {
867
+ const next = new Set(current);
868
+ next.delete(appId);
869
+ return next;
870
+ });
871
+ });
872
+ }
873
+ async function persistBotAssignment(appId, skillNames, packIdsList) {
781
874
  setBotStatuses(statuses => ({ ...statuses, [appId]: null }));
782
875
  try {
876
+ // Single merge-write: direct skills + packs are saved together to avoid two
877
+ // full-policy PUTs racing and silently overwriting each other. Unknown
878
+ // selectors (e.g. set via CLI) are retained for forward/downgrade compat.
879
+ const currentBot = botsRef.current.find(bot => bot.larkAppId === appId);
880
+ const mergedInclude = mergeBotAssignmentSelectors(currentBot?.skills, skillNames, packIdsList);
783
881
  const body = await jsonRequest(`/api/bots/${encodeURIComponent(appId)}/skills`, {
784
882
  method: 'PUT',
785
883
  body: JSON.stringify({
786
884
  action: 'set',
787
- policy: names.length > 0 ? { include: names.map(name => `skill:${name}`) } : null,
885
+ policy: mergedInclude.length > 0 ? { include: mergedInclude } : null,
788
886
  }),
789
887
  });
790
888
  if (!mountedRef.current)
791
889
  return;
792
- setBots(rows => rows.map(bot => bot.larkAppId === appId ? { ...bot, skills: body.skills ?? null } : bot));
890
+ const updateBot = (rows) => rows.map(bot => (bot.larkAppId === appId ? { ...bot, skills: body.skills ?? null } : bot));
891
+ botsRef.current = updateBot(botsRef.current);
892
+ setBots(updateBot);
793
893
  setBotStatuses(statuses => ({
794
894
  ...statuses,
795
895
  [appId]: { text: tr('skills.policySaved'), ok: true },
@@ -804,31 +904,76 @@ function SkillsPage() {
804
904
  }
805
905
  throw err;
806
906
  }
807
- finally {
808
- if (mountedRef.current)
809
- setBotBusy(null);
907
+ }
908
+ function setBotAssignment(appId, skillNames, packIdsList) {
909
+ return enqueueBotAssignment(appId, () => persistBotAssignment(appId, skillNames, packIdsList));
910
+ }
911
+ function mutateBotAssignment(appId, selector, present) {
912
+ return enqueueBotAssignment(appId, async () => {
913
+ const currentBot = botsRef.current.find(bot => bot.larkAppId === appId);
914
+ const skillNames = priorityNames(currentBot?.skills);
915
+ const packIdsList = packIds(currentBot?.skills);
916
+ const [kind, value] = selector.split(':', 2);
917
+ const update = (values) => present
918
+ ? [...new Set([...values, value])]
919
+ : values.filter(item => item !== value);
920
+ await persistBotAssignment(appId, kind === 'skill' ? update(skillNames) : skillNames, kind === 'pack' ? update(packIdsList) : packIdsList);
921
+ });
922
+ }
923
+ async function setBotInjection(appId, value) {
924
+ setBotStatuses(statuses => ({ ...statuses, [appId]: null }));
925
+ try {
926
+ await jsonRequest(`/api/bots/${encodeURIComponent(appId)}/skill-injection`, {
927
+ method: 'PUT',
928
+ body: JSON.stringify({ skillInjection: value }),
929
+ });
930
+ if (!mountedRef.current)
931
+ return;
932
+ setBots(rows => rows.map(bot => bot.larkAppId === appId ? { ...bot, skillInjection: value } : bot));
933
+ setBotStatuses(statuses => ({ ...statuses, [appId]: { text: tr('skills.saved'), ok: true } }));
934
+ }
935
+ catch (err) {
936
+ if (mountedRef.current) {
937
+ setBotStatuses(statuses => ({ ...statuses, [appId]: { text: `${tr('skills.failed')}: ${err?.message ?? err}`, ok: false } }));
938
+ }
810
939
  }
811
940
  }
812
941
  const configuredBotCount = bots.filter(bot => policyConfigured(bot.skills)).length;
813
- const attachedSkillRefCount = bots.reduce((sum, bot) => sum + policyReferenceCount(bot.skills), 0);
814
- const headingActions = (_jsxs("div", { className: "page-heading-actions skills-heading-actions", children: [_jsxs("div", { className: "skills-metric-strip", children: [_jsxs("span", { children: [_jsx("small", { children: tr('skills.metricInstalled') }), _jsx("strong", { children: skills.length })] }), _jsxs("span", { children: [_jsx("small", { children: tr('skills.metricBots') }), _jsxs("strong", { children: [configuredBotCount, "/", bots.length] })] }), _jsxs("span", { children: [_jsx("small", { children: tr('skills.metricAttached') }), _jsx("strong", { children: attachedSkillRefCount })] })] }), _jsx(RefreshIconButton, { id: "skills-refresh", label: tr('skills.refresh'), busy: loading, disabled: loading, onClick: () => void refresh() })] }));
815
- const body = (_jsx("div", { className: "skills-page-stack", children: loading ? _jsx(LoadingState, { label: tr('common.loading') }) : loadError ? _jsx("p", { className: "hint-warn", children: loadError }) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "skills-config-row", children: [_jsxs("section", { className: "skills-config-block", children: [_jsx(SectionHeader, { title: tr('skills.globalDefaults') }), _jsxs("article", { className: "bd-card skills-defaults-panel skills-config-card", children: [_jsxs("div", { className: "skills-control-block", children: [_jsx("span", { className: "skills-control-label", children: tr('skills.globalProject') }), _jsx(SkillSegmented, { value: trustProjectSkills, disabled: globalBusy === 'project', options: [
816
- { value: 'off', label: tr('skills.globalProjectOff'), help: tr('skills.globalProjectOffHelp') },
817
- { value: 'all', label: tr('skills.globalProjectAll'), help: tr('skills.globalProjectAllHelp') },
818
- ], onChange: value => void updateGlobalProject(value) })] }), _jsxs("div", { className: "skills-control-block", children: [_jsx("span", { className: "skills-control-label", children: tr('skills.globalDelivery') }), _jsx(SkillSegmented, { value: delivery, disabled: globalBusy === 'delivery', options: [
819
- { value: 'auto', label: tr('skills.deliveryAuto'), help: tr('skills.deliveryAutoHelp') },
820
- { value: 'prompt', label: tr('skills.deliveryPrompt'), help: tr('skills.deliveryPromptHelp') },
821
- { value: 'native', label: tr('skills.deliveryNative'), help: tr('skills.deliveryNativeHelp') },
822
- ], onChange: value => void updateGlobalDelivery(value) })] })] })] }), _jsxs("section", { className: "skills-config-block", children: [_jsx(SectionHeader, { title: _jsx(FieldTitle, { help: tr('skills.installInfo'), helpLabel: tr('skills.installInfoLabel'), children: tr('skills.install') }) }), _jsx(SkillsInstallPanel, { showTitle: false, installSource: installSource, installPath: installPath, installRef: installRef, installStatus: installStatus, installBusy: installBusy, installDiscovering: installDiscovering, installSelectionOpen: installSelectionOpen, installCandidates: installCandidates, selectedInstallSkills: selectedInstallSkills, onInstallSourceChange: (value) => {
823
- setInstallSource(value);
824
- clearInstallDiscovery();
825
- }, onInstallPathChange: (value) => {
826
- setInstallPath(value);
827
- clearInstallDiscovery();
828
- }, onInstallRefChange: (value) => {
829
- setInstallRef(value);
830
- clearInstallDiscovery();
831
- }, onToggleInstallSkill: toggleInstallCandidate, onSelectAllInstallSkills: selectAllInstallCandidates, onConfirmInstallSelection: () => void confirmInstallSelection(), onCloseInstallSelection: () => setInstallSelectionOpen(false), onInstall: () => void installSkill(), onOpenNativeDiscovery: () => setDiscoveryOpen(true) })] }), _jsxs("section", { className: "skills-config-block", children: [_jsx(SectionHeader, { title: tr('skills.bots'), count: tr('skills.botCount', { count: bots.length }), hint: tr('skills.botsHelp') }), _jsx("section", { className: "bd-card skills-bots-panel skills-config-card", children: _jsx("div", { className: "skills-bot-grid", children: bots.map(bot => (_jsx(BotPolicyCard, { bot: bot, installedNames: installedNames, skills: skills, status: botStatuses[bot.larkAppId] ?? null, busyKey: botBusy, onSave: setBotSkills }, bot.larkAppId))) }) })] })] }), _jsx(InstalledSkillsLibrary, { skills: skills, busySkill: skillBusy, removingNames: removingNames, status: installedStatus, onUpdate: name => void updateSkill(name), onRequestRemove: requestSkillRemoval }), _jsx(RemoveSkillsDialog, { names: removalDialogOpen ? pendingRemoval : null, references: removalReferences, busy: removingNames.size > 0, error: removalError, onCancel: cancelSkillRemoval, onConfirm: force => void confirmSkillRemoval(force) }), _jsx("dialog", { className: "skills-discovery-dialog", id: "skills-discovery-dialog", ref: discoveryDialogRef, onClose: () => setDiscoveryOpen(false), onClick: event => {
942
+ // Count every policy selector (skill: AND pack:), not just direct skills
943
+ // otherwise pack-only bots show 0 despite being configured.
944
+ const attachedSkillRefCount = bots.reduce((sum, bot) => sum + (bot.skills?.include?.length ?? 0), 0);
945
+ // Single relationship model for the whole page: skill ↔ pack ↔ bot edges,
946
+ // including skills that are referenced but not installed.
947
+ const skillGraph = useMemo(() => buildSkillGraph(skills, packs, bots, { packsKnown }), [skills, packs, bots, packsKnown]);
948
+ const danglingSkills = useMemo(() => danglingSkillNames(skillGraph), [skillGraph]);
949
+ const unassignedPackIds = useMemo(() => [...skillGraph.packs.values()].filter(pack => pack.botIds.length === 0).map(pack => pack.id), [skillGraph]);
950
+ const botsWithIssues = useMemo(() => [...skillGraph.bots.values()]
951
+ .filter(bot => bot.health === 'missing' || bot.health === 'pack_missing')
952
+ .map(bot => bot.larkAppId), [skillGraph]);
953
+ const headingActions = (_jsxs("div", { className: "page-heading-actions skills-heading-actions", children: [_jsxs("div", { className: "skills-metric-strip", children: [_jsxs("span", { children: [_jsx("small", { children: tr('skills.metricInstalled') }), _jsx("strong", { children: skills.length })] }), _jsxs("span", { children: [_jsx("small", { children: tr('skills.metricPacks') }), _jsx("strong", { children: packsKnown ? packs.length : '—' })] }), _jsxs("span", { children: [_jsx("small", { children: tr('skills.metricBots') }), _jsxs("strong", { children: [configuredBotCount, "/", bots.length] })] }), _jsxs("span", { children: [_jsx("small", { children: tr('skills.metricAttached') }), _jsx("strong", { children: attachedSkillRefCount })] })] }), _jsxs("div", { className: "skills-issue-strip", role: "group", "aria-label": tr('skills.issuesLabel'), children: [danglingSkills.length > 0 && (_jsx("button", { type: "button", className: "skills-issue-badge skills-issue-warn", "data-action": "issue-dangling-skills", title: danglingSkills.join(', '), onClick: () => navigateTo(danglingSkills.length === 1
954
+ ? {
955
+ tab: 'library',
956
+ librarySearch: danglingSkills[0],
957
+ openInstallWizard: true,
958
+ installTargetSkill: danglingSkills[0],
959
+ }
960
+ : { tab: 'library', libraryFilterSkills: danglingSkills }), children: tr('skills.issueDangling', { count: danglingSkills.length }) })), unassignedPackIds.length > 0 && (_jsx("button", { type: "button", className: "skills-issue-badge skills-issue-info", "data-action": "issue-unassigned-packs", onClick: () => navigateTo({ tab: 'packs', focusPackIds: unassignedPackIds }), children: tr('skills.issueUnassignedPacks', { count: unassignedPackIds.length }) })), botsWithIssues.length > 0 && (_jsx("button", { type: "button", className: "skills-issue-badge skills-issue-warn", "data-action": "issue-bots-missing", onClick: () => navigateTo({ tab: 'bots', focusBotIds: botsWithIssues }), children: tr('skills.issueBotsMissing', { count: botsWithIssues.length }) })), !loading && packsKnown && danglingSkills.length === 0 && unassignedPackIds.length === 0 && botsWithIssues.length === 0 && (_jsx("span", { className: "skills-issue-badge skills-issue-ok", "data-skills-healthy": true, children: tr('skills.issueNone') }))] }), _jsx(RefreshIconButton, { id: "skills-refresh", label: tr('skills.refresh'), busy: loading, disabled: loading, onClick: () => void refresh() })] }));
961
+ const body = (_jsx("div", { className: "skills-page-stack", children: loading ? _jsx(LoadingState, { label: tr('common.loading') }) : loadError ? _jsx("p", { className: "hint-warn", children: loadError }) : (_jsxs(_Fragment, { children: [packsError && (_jsx("p", { className: "hint-warn", "data-packs-error": true, role: "alert", children: tr(packsKnown ? 'skills.packsLoadError' : 'skills.packsLoadErrorFirst', { error: packsError }) })), _jsxs("div", { className: "skills-tabs", role: "tablist", children: [_jsx("button", { role: "tab", "aria-selected": activeTab === 'library', className: activeTab === 'library' ? 'active' : '', onClick: () => setActiveTab('library'), children: tr('skills.tabLibrary') }), _jsx("button", { role: "tab", "aria-selected": activeTab === 'packs', className: activeTab === 'packs' ? 'active' : '', onClick: () => setActiveTab('packs'), children: tr('skills.tabPacks') }), _jsx("button", { role: "tab", "aria-selected": activeTab === 'bots', className: activeTab === 'bots' ? 'active' : '', onClick: () => setActiveTab('bots'), children: tr('skills.tabBots') }), _jsx("button", { role: "tab", "aria-selected": activeTab === 'delivery', className: activeTab === 'delivery' ? 'active' : '', onClick: () => setActiveTab('delivery'), children: tr('skills.tabDelivery') })] }), activeTab === 'packs' && (_jsx(SkillPacksTab, { skills: skills, packs: packs, packsKnown: packsKnown, onRefresh: () => void refresh(), focusPackId: navIntent?.tab === 'packs' ? navIntent.focusPackId ?? null : null, focusPackIds: navIntent?.tab === 'packs' ? navIntent.focusPackIds ?? null : null, onFocusConsumed: consumeNavIntent, onOpenBot: botId => navigateTo({ tab: 'bots', focusBotIds: [botId] }), onInstallMissingSkill: name => navigateTo({
962
+ tab: 'library',
963
+ librarySearch: name,
964
+ openInstallWizard: true,
965
+ installTargetSkill: name,
966
+ }) })), activeTab === 'library' && (_jsx(SkillLibraryTab, { skills: skills, nativeSkillGroups: nativeSkillGroups, installSource: installSource, installPath: installPath, installRef: installRef, installFullDepth: installForceFullDepth, installStatus: installStatus, installBusy: installBusy, installDiscovering: installDiscovering, installSelectionOpen: installSelectionOpen, installCandidates: installCandidates, selectedInstallSkills: selectedInstallSkills, onInstallSourceChange: (value) => { setInstallSource(value); clearInstallDiscovery(); }, onInstallPathChange: (value) => { setInstallPath(value); clearInstallDiscovery(); }, onInstallRefChange: (value) => { setInstallRef(value); clearInstallDiscovery(); }, onInstallFullDepthChange: (value) => { setInstallForceFullDepth(value); clearInstallDiscovery(); }, onToggleInstallSkill: toggleInstallCandidate, onSelectAllInstallSkills: selectAllInstallCandidates, onConfirmInstallSelection: confirmInstallSelection, onCloseInstallSelection: () => setInstallSelectionOpen(false), onInstall: installSkill, onOpenNativeDiscovery: () => setDiscoveryOpen(true), onCreatePack: createPackFromInstalledSkills, InstallPanel: SkillsInstallPanel, InstalledLibrary: InstalledSkillsLibrary, RemoveDialog: RemoveSkillsDialog, removingNames: removingNames, removalDialogOpen: removalDialogOpen, pendingRemoval: pendingRemoval, removalReferences: removalReferences, removalError: removalError, skillBusy: skillBusy, installedStatus: installedStatus, onUpdateSkill: name => void updateSkill(name), onRequestRemove: requestSkillRemoval, onCancelRemoval: cancelSkillRemoval, onConfirmRemoval: force => void confirmSkillRemoval(force), navIntent: navIntent?.tab === 'library' ? navIntent : null, onNavIntentConsumed: consumeNavIntent, installTargetSkill: installTargetSkill, onClearInstallTarget: () => setInstallTargetSkill(null), packsKnown: packsKnown, onInstallMissing: name => navigateTo({
967
+ tab: 'library',
968
+ openInstallWizard: true,
969
+ installTargetSkill: name,
970
+ }), graph: skillGraph, packNames: packs.map(pack => ({ id: pack.id, name: pack.name })), onShowSkillPacks: name => {
971
+ const packIdsOfSkill = skillGraph.skills.get(name)?.packIds ?? [];
972
+ // One pack → open its editor; several → highlight them all.
973
+ navigateTo(packIdsOfSkill.length === 1
974
+ ? { tab: 'packs', focusPackId: packIdsOfSkill[0] }
975
+ : { tab: 'packs', focusPackIds: packIdsOfSkill });
976
+ }, onShowSkillBots: name => navigateTo({ tab: 'bots', focusSkill: name }) })), activeTab === 'bots' && (_jsx(BotAssignmentsTab, { bots: bots, skills: skills, statuses: botStatuses, onSave: setBotAssignment, onMutate: mutateBotAssignment, busyBotIds: busyBotIds, packs: packs, packsKnown: packsKnown, focusBotIds: navIntent?.tab === 'bots' ? navIntent.focusBotIds ?? null : null, focusSkill: navIntent?.tab === 'bots' ? navIntent.focusSkill ?? null : null, onFocusConsumed: consumeNavIntent, onOpenPack: packId => navigateTo({ tab: 'packs', focusPackId: packId }), onOpenSkill: name => navigateTo({ tab: 'library', librarySearch: name }) })), activeTab === 'delivery' && (_jsx(DeliverySettingsTab, { trustProjectSkills: trustProjectSkills, delivery: delivery, globalBusy: globalBusy, onUpdateProject: value => void updateGlobalProject(value), onUpdateDelivery: value => void updateGlobalDelivery(value), bots: bots, onUpdateBotInjection: setBotInjection, botStatuses: botStatuses, SkillSegmented: SkillSegmented })), _jsx("dialog", { className: "skills-discovery-dialog", id: "skills-discovery-dialog", ref: discoveryDialogRef, onClose: () => setDiscoveryOpen(false), onClick: event => {
832
977
  if (event.target === event.currentTarget)
833
978
  setDiscoveryOpen(false);
834
979
  }, children: _jsxs("article", { children: [_jsxs("header", { children: [_jsx("h3", { children: tr('skills.discoverTitle') }), _jsx("p", { children: tr('skills.discoverHelp') }), _jsx("button", { type: "button", className: "skills-discovery-close", "data-action": "close-discovery", "aria-label": tr('skills.discoverClose'), title: tr('skills.discoverClose'), onClick: () => setDiscoveryOpen(false) })] }), _jsx("div", { className: "skills-discovery-body", children: !activeDiscoveryGroup ? _jsx("p", { className: "empty", children: tr('skills.discoverEmpty') }) : (_jsx(_Fragment, { children: _jsxs("div", { className: "skills-discovery-layout", children: [_jsx("div", { className: "skills-discovery-tabs", role: "tablist", "aria-label": tr('skills.discoverTitle'), children: nativeSkillGroups.map(group => {