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.
- package/dist/.runtime-build-id +1 -1
- package/dist/adapters/backend/riff-backend.d.ts +18 -0
- package/dist/adapters/backend/riff-backend.d.ts.map +1 -1
- package/dist/adapters/backend/riff-backend.js +143 -0
- package/dist/adapters/backend/riff-backend.js.map +1 -1
- package/dist/adapters/cli/kimi.d.ts.map +1 -1
- package/dist/adapters/cli/kimi.js +22 -8
- package/dist/adapters/cli/kimi.js.map +1 -1
- package/dist/adapters/cli/pi.d.ts.map +1 -1
- package/dist/adapters/cli/pi.js +3 -1
- package/dist/adapters/cli/pi.js.map +1 -1
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +4 -1
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +65 -27
- package/dist/cli.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +16 -2
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/dashboard-ipc-server.js +1 -1
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/dashboard-rows.d.ts +17 -1
- package/dist/core/dashboard-rows.d.ts.map +1 -1
- package/dist/core/dashboard-rows.js +13 -1
- package/dist/core/dashboard-rows.js.map +1 -1
- package/dist/core/maintenance.d.ts.map +1 -1
- package/dist/core/maintenance.js +5 -0
- package/dist/core/maintenance.js.map +1 -1
- package/dist/core/skills/cli-admin-command.d.ts.map +1 -1
- package/dist/core/skills/cli-admin-command.js +142 -3
- package/dist/core/skills/cli-admin-command.js.map +1 -1
- package/dist/core/skills/frontmatter.d.ts.map +1 -1
- package/dist/core/skills/frontmatter.js +44 -7
- package/dist/core/skills/frontmatter.js.map +1 -1
- package/dist/core/skills/im-command.d.ts +6 -0
- package/dist/core/skills/im-command.d.ts.map +1 -1
- package/dist/core/skills/im-command.js +34 -7
- package/dist/core/skills/im-command.js.map +1 -1
- package/dist/core/skills/install-command.d.ts +4 -0
- package/dist/core/skills/install-command.d.ts.map +1 -0
- package/dist/core/skills/install-command.js +89 -0
- package/dist/core/skills/install-command.js.map +1 -0
- package/dist/core/skills/package.d.ts.map +1 -1
- package/dist/core/skills/package.js +10 -3
- package/dist/core/skills/package.js.map +1 -1
- package/dist/core/skills/policy.d.ts +5 -1
- package/dist/core/skills/policy.d.ts.map +1 -1
- package/dist/core/skills/policy.js +66 -3
- package/dist/core/skills/policy.js.map +1 -1
- package/dist/core/skills/references.d.ts +9 -1
- package/dist/core/skills/references.d.ts.map +1 -1
- package/dist/core/skills/references.js +35 -2
- package/dist/core/skills/references.js.map +1 -1
- package/dist/core/skills/registry-paths.d.ts +1 -0
- package/dist/core/skills/registry-paths.d.ts.map +1 -1
- package/dist/core/skills/registry-paths.js +3 -0
- package/dist/core/skills/registry-paths.js.map +1 -1
- package/dist/core/skills/session-resolver.d.ts +2 -1
- package/dist/core/skills/session-resolver.d.ts.map +1 -1
- package/dist/core/skills/session-resolver.js +1 -0
- package/dist/core/skills/session-resolver.js.map +1 -1
- package/dist/core/skills/session-runtime.d.ts.map +1 -1
- package/dist/core/skills/session-runtime.js +2 -0
- package/dist/core/skills/session-runtime.js.map +1 -1
- package/dist/core/skills/sources.d.ts +23 -0
- package/dist/core/skills/sources.d.ts.map +1 -1
- package/dist/core/skills/sources.js +128 -12
- package/dist/core/skills/sources.js.map +1 -1
- package/dist/core/skills/types.d.ts +22 -1
- package/dist/core/skills/types.d.ts.map +1 -1
- package/dist/core/types.d.ts +2 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +52 -1
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/dashboard/bot-onboarding.d.ts.map +1 -1
- package/dist/dashboard/bot-onboarding.js +2 -114
- package/dist/dashboard/bot-onboarding.js.map +1 -1
- package/dist/dashboard/connector-api.d.ts.map +1 -1
- package/dist/dashboard/connector-api.js +66 -6
- package/dist/dashboard/connector-api.js.map +1 -1
- package/dist/dashboard/public-redact.d.ts.map +1 -1
- package/dist/dashboard/public-redact.js +3 -0
- package/dist/dashboard/public-redact.js.map +1 -1
- package/dist/dashboard/skill-install-request.js +10 -11
- package/dist/dashboard/skill-install-request.js.map +1 -1
- package/dist/dashboard/skill-pack-response.d.ts +15 -0
- package/dist/dashboard/skill-pack-response.d.ts.map +1 -0
- package/dist/dashboard/skill-pack-response.js +32 -0
- package/dist/dashboard/skill-pack-response.js.map +1 -0
- package/dist/dashboard/web/connectors-page.d.ts +69 -0
- package/dist/dashboard/web/connectors-page.d.ts.map +1 -1
- package/dist/dashboard/web/connectors-page.js +47 -7
- package/dist/dashboard/web/connectors-page.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +301 -26
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/preferences.d.ts +4 -2
- package/dist/dashboard/web/preferences.d.ts.map +1 -1
- package/dist/dashboard/web/preferences.js +14 -6
- package/dist/dashboard/web/preferences.js.map +1 -1
- package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
- package/dist/dashboard/web/sessions-page.js +99 -3
- package/dist/dashboard/web/sessions-page.js.map +1 -1
- package/dist/dashboard/web/sessions.d.ts +3 -1
- package/dist/dashboard/web/sessions.d.ts.map +1 -1
- package/dist/dashboard/web/sessions.js +13 -6
- package/dist/dashboard/web/sessions.js.map +1 -1
- package/dist/dashboard/web/skills/bot-assignments-tab.d.ts +27 -0
- package/dist/dashboard/web/skills/bot-assignments-tab.d.ts.map +1 -0
- package/dist/dashboard/web/skills/bot-assignments-tab.js +257 -0
- package/dist/dashboard/web/skills/bot-assignments-tab.js.map +1 -0
- package/dist/dashboard/web/skills/delivery-settings-tab.d.ts +25 -0
- package/dist/dashboard/web/skills/delivery-settings-tab.d.ts.map +1 -0
- package/dist/dashboard/web/skills/delivery-settings-tab.js +20 -0
- package/dist/dashboard/web/skills/delivery-settings-tab.js.map +1 -0
- package/dist/dashboard/web/skills/shared.d.ts +86 -0
- package/dist/dashboard/web/skills/shared.d.ts.map +1 -0
- package/dist/dashboard/web/skills/shared.js +178 -0
- package/dist/dashboard/web/skills/shared.js.map +1 -0
- package/dist/dashboard/web/skills/skill-library-tab.d.ts +75 -0
- package/dist/dashboard/web/skills/skill-library-tab.d.ts.map +1 -0
- package/dist/dashboard/web/skills/skill-library-tab.js +108 -0
- package/dist/dashboard/web/skills/skill-library-tab.js.map +1 -0
- package/dist/dashboard/web/skills/skill-packs-tab.d.ts +21 -0
- package/dist/dashboard/web/skills/skill-packs-tab.d.ts.map +1 -0
- package/dist/dashboard/web/skills/skill-packs-tab.js +185 -0
- package/dist/dashboard/web/skills/skill-packs-tab.js.map +1 -0
- package/dist/dashboard/web/skills/types.d.ts +95 -0
- package/dist/dashboard/web/skills/types.d.ts.map +1 -0
- package/dist/dashboard/web/skills/types.js +2 -0
- package/dist/dashboard/web/skills/types.js.map +1 -0
- package/dist/dashboard/web/skills/use-skills-data.d.ts +31 -0
- package/dist/dashboard/web/skills/use-skills-data.d.ts.map +1 -0
- package/dist/dashboard/web/skills/use-skills-data.js +86 -0
- package/dist/dashboard/web/skills/use-skills-data.js.map +1 -0
- package/dist/dashboard/web/skills-page.d.ts +34 -31
- package/dist/dashboard/web/skills-page.d.ts.map +1 -1
- package/dist/dashboard/web/skills-page.js +321 -176
- package/dist/dashboard/web/skills-page.js.map +1 -1
- package/dist/dashboard/webhook-routes.d.ts +18 -0
- package/dist/dashboard/webhook-routes.d.ts.map +1 -1
- package/dist/dashboard/webhook-routes.js +35 -1
- package/dist/dashboard/webhook-routes.js.map +1 -1
- package/dist/dashboard-web/app.js +1 -1
- package/dist/dashboard-web/chunks/{bot-defaults-page-USC6EZXP.js → bot-defaults-page-HL655CNR.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-KOPISPHN.js → chunk-5PAYQ3KT.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-RXRJB5SO.js → chunk-5ZL7NLR5.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-A7VC4BXR.js → chunk-BFUTTYVK.js} +1 -1
- package/dist/dashboard-web/chunks/chunk-ELL53D62.js +55 -0
- package/dist/dashboard-web/chunks/connectors-page-3QQACF23.js +1 -0
- package/dist/dashboard-web/chunks/{groups-page-C6NSDTIN.js → groups-page-SDZMJ7UV.js} +1 -1
- package/dist/dashboard-web/chunks/{insights-page-UTOVYLLY.js → insights-page-ZZ3JEJ6L.js} +1 -1
- package/dist/dashboard-web/chunks/{monitor-room-UKTJYTA2.js → monitor-room-ROJ5GZ4N.js} +1 -1
- package/dist/dashboard-web/chunks/{monitoring-page-Q3CEZIVX.js → monitoring-page-IATV7KJF.js} +1 -1
- package/dist/dashboard-web/chunks/{office-page-GYCZOOVJ.js → office-page-TYO5OIIT.js} +1 -1
- package/dist/dashboard-web/chunks/{overview-page-W2VBJ6OH.js → overview-page-VJUULZF2.js} +1 -1
- package/dist/dashboard-web/chunks/{roles-page-U4SHER4K.js → roles-page-7RJJKZGK.js} +1 -1
- package/dist/dashboard-web/chunks/{schedules-page-TNPW3N3Q.js → schedules-page-JTEJ5IXW.js} +1 -1
- package/dist/dashboard-web/chunks/sessions-page-KD7HDT3U.js +8 -0
- package/dist/dashboard-web/chunks/{settings-page-VKW5ZK5K.js → settings-page-7CTTFBRS.js} +1 -1
- package/dist/dashboard-web/chunks/skills-page-57NZECJ3.js +4 -0
- package/dist/dashboard-web/chunks/{team-federation-page-OMFGGBT2.js → team-federation-page-6YYIMJG3.js} +1 -1
- package/dist/dashboard-web/chunks/{v3-page-PLXWK4KO.js → v3-page-ZGKBSBK3.js} +1 -1
- package/dist/dashboard-web/chunks/{whiteboards-page-NJQOEO2P.js → whiteboards-page-AACJF6DB.js} +1 -1
- package/dist/dashboard-web/style.css +1623 -160
- package/dist/dashboard.js +200 -16
- package/dist/dashboard.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +2 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +2 -0
- package/dist/i18n/zh.js.map +1 -1
- package/dist/index-daemon.js +6 -1
- package/dist/index-daemon.js.map +1 -1
- package/dist/services/connector-store.d.ts +14 -1
- package/dist/services/connector-store.d.ts.map +1 -1
- package/dist/services/connector-store.js.map +1 -1
- package/dist/services/connector-topic-template.d.ts +7 -0
- package/dist/services/connector-topic-template.d.ts.map +1 -0
- package/dist/services/connector-topic-template.js +210 -0
- package/dist/services/connector-topic-template.js.map +1 -0
- package/dist/services/project-scanner.d.ts +33 -0
- package/dist/services/project-scanner.d.ts.map +1 -1
- package/dist/services/project-scanner.js +40 -1
- package/dist/services/project-scanner.js.map +1 -1
- package/dist/services/skill-pack-store.d.ts +50 -0
- package/dist/services/skill-pack-store.d.ts.map +1 -0
- package/dist/services/skill-pack-store.js +270 -0
- package/dist/services/skill-pack-store.js.map +1 -0
- package/dist/services/skill-registry-store.d.ts +8 -0
- package/dist/services/skill-registry-store.d.ts.map +1 -1
- package/dist/services/skill-registry-store.js +100 -68
- package/dist/services/skill-registry-store.js.map +1 -1
- package/dist/services/todo-state.d.ts +74 -0
- package/dist/services/todo-state.d.ts.map +1 -0
- package/dist/services/todo-state.js +351 -0
- package/dist/services/todo-state.js.map +1 -0
- package/dist/setup/agent-preset.d.ts.map +1 -1
- package/dist/setup/agent-preset.js +2 -0
- package/dist/setup/agent-preset.js.map +1 -1
- package/dist/setup/owner-identity.d.ts +47 -0
- package/dist/setup/owner-identity.d.ts.map +1 -0
- package/dist/setup/owner-identity.js +196 -0
- package/dist/setup/owner-identity.js.map +1 -0
- package/dist/setup/register-app.d.ts +4 -3
- package/dist/setup/register-app.d.ts.map +1 -1
- package/dist/setup/register-app.js.map +1 -1
- package/dist/setup/setup-args.d.ts +1 -1
- package/dist/setup/setup-args.d.ts.map +1 -1
- package/dist/setup/setup-args.js +7 -3
- package/dist/setup/setup-args.js.map +1 -1
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/child-env.d.ts +25 -0
- package/dist/utils/child-env.d.ts.map +1 -1
- package/dist/utils/child-env.js +47 -0
- package/dist/utils/child-env.js.map +1 -1
- package/dist/worker.js +55 -16
- package/dist/worker.js.map +1 -1
- package/dist/workflows/definition.d.ts +44 -44
- package/package.json +1 -1
- package/dist/dashboard-web/chunks/chunk-M6BHWQFO.js +0 -51
- package/dist/dashboard-web/chunks/connectors-page-4Z76HWRH.js +0 -1
- package/dist/dashboard-web/chunks/sessions-page-YWWDDV57.js +0 -6
- 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
|
-
|
|
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
|
|
122
|
-
return
|
|
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
|
-
|
|
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) }),
|
|
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
|
-
?
|
|
282
|
-
|
|
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 }),
|
|
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
|
-
|
|
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
|
|
299
|
-
const [
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const [
|
|
304
|
-
|
|
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 [
|
|
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
|
|
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
|
|
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
|
-
|
|
506
|
-
|
|
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
|
-
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
779
|
-
const
|
|
780
|
-
|
|
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:
|
|
885
|
+
policy: mergedInclude.length > 0 ? { include: mergedInclude } : null,
|
|
788
886
|
}),
|
|
789
887
|
});
|
|
790
888
|
if (!mountedRef.current)
|
|
791
889
|
return;
|
|
792
|
-
|
|
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
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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
|
-
|
|
814
|
-
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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 => {
|