diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b0047dbd48..0e6761ee18 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -451,8 +451,6 @@ prerequisites: # Optional legacy runtime requirements commands: [curl, jq] # Advisory only; does not hide the skill metadata: hermes: - editorial_name: My Skill # Optional human-readable UI title - editorial_description: What this skill helps a person accomplish. tags: [Category, Subcategory, Keywords] related_skills: [other-skill-name] fallback_for_toolsets: [web] # Optional — show only when toolset is unavailable @@ -485,12 +483,6 @@ Known failure modes and how to handle them. How the agent confirms it worked. ``` -`metadata.hermes.editorial_name` and `editorial_description` are optional, -human-facing presentation copy. They may use natural titles and fuller prose -than the routing-focused top-level fields. Hermes continues to identify and -route skills with `name` and `description`; UIs fall back to that canonical -pair when editorial copy is absent. - ### Platform-specific skills Skills can declare which OS platforms they support via the `platforms` frontmatter field. Skills with this field are automatically hidden from the system prompt, `skills_list()`, and slash commands on incompatible platforms. diff --git a/agent/learn_prompt.py b/agent/learn_prompt.py index 676007af7f..db50f95aa3 100644 --- a/agent/learn_prompt.py +++ b/agent/learn_prompt.py @@ -38,12 +38,6 @@ Frontmatter: cross-platform first (tempfile.gettempdir(), pathlib.Path, psutil); gate only when the dependency is genuinely platform-bound. Omit the field for portable skills. -- metadata.hermes.editorial_name: a concise, human-readable title for app - surfaces. Use normal title casing and spaces; this is presentation copy, not - the agent-facing skill identifier. -- metadata.hermes.editorial_description: one or two plain-language sentences - explaining the skill to a person browsing it. This is presentation copy and - does not replace the routing-focused top-level description. - metadata.hermes.tags: a few Capitalized, Relevant, Tags. Body section order (omit a section only if it genuinely has no content): diff --git a/agent/skill_utils.py b/agent/skill_utils.py index 9c06c05df2..fe0b06c69f 100644 --- a/agent/skill_utils.py +++ b/agent/skill_utils.py @@ -33,12 +33,6 @@ ORG_ACTIVE_MARKER = ".active_org" ORG_PROVENANCE_FILE = ".org-provenance.json" ORG_BASELINE_FILE = ".org-baseline.json" # upstream fingerprint; detects local edits -# Collective Wisdom managed installs are intentionally separate from the M2 -# whole-org mirror. The only writer of this marker is the Wisdom setup/client -# path after the Gateway has accepted the profile's installation identity. -WISDOM_MANAGED_DIR_NAME = "_wisdom" -WISDOM_ACTIVE_MARKER = ".active_org" - def read_active_org_id(skills_dir: Path) -> Optional[str]: """The org id whose mirror may resolve, or None (no org skills load).""" @@ -49,27 +43,6 @@ def read_active_org_id(skills_dir: Path) -> Optional[str]: return None -def read_active_wisdom_org_id(skills_dir: Path) -> Optional[str]: - """The last Gateway-verified org whose managed Wisdom skills may load.""" - try: - marker = skills_dir / WISDOM_MANAGED_DIR_NAME / WISDOM_ACTIVE_MARKER - if not marker.exists(): - return None - value = marker.read_text(encoding="utf-8").strip() - return value or None - except OSError: - return None - - -def is_wisdom_managed_path(path, skills_dir: Path) -> bool: - """True when *path* is below ``_wisdom//``.""" - try: - rel = Path(path).resolve().relative_to(Path(skills_dir).resolve()) - except (OSError, ValueError): - return False - return bool(rel.parts) and rel.parts[0] == WISDOM_MANAGED_DIR_NAME - - def _org_rel_parts(path, skills_dir: Path) -> Tuple[str, ...]: """Path parts of *path* relative to *skills_dir* if it is under ``_org/``, else ``()``.""" try: @@ -760,113 +733,22 @@ def is_skill_description_truncated_for_prompt(frontmatter: Dict[str, Any]) -> bo return len(_normalize_skill_description(frontmatter)) > SKILL_PROMPT_DESC_LIMIT -def extract_skill_editorial_metadata( - frontmatter: Dict[str, Any], - *, - fallback_name: str, - fallback_description: str, -) -> Dict[str, str]: - """Resolve optional human-facing skill copy without changing agent metadata. - - ``name`` and ``description`` remain the canonical agent-facing routing - fields. Hermes UIs may use the optional values under ``metadata.hermes``; - older and third-party skills fall back to the canonical pair. - """ - metadata = frontmatter.get("metadata") - hermes = metadata.get("hermes") if isinstance(metadata, dict) else None - if not isinstance(hermes, dict): - hermes = {} - - editorial_name = hermes.get("editorial_name") - editorial_description = hermes.get("editorial_description") - return { - "editorial_name": ( - editorial_name.strip() - if isinstance(editorial_name, str) and editorial_name.strip() - else fallback_name - ), - "editorial_description": ( - editorial_description.strip() - if isinstance(editorial_description, str) - and editorial_description.strip() - else fallback_description - ), - } - - -def load_skill_editorial_metadata( - skill_path: Path, - *, - fallback_name: str | None = None, - fallback_description: str = "", -) -> Dict[str, str]: - """Load human-facing copy from a skill directory with safe fallbacks.""" - canonical_name = fallback_name or skill_path.name - canonical_description = fallback_description - try: - frontmatter, _body = parse_frontmatter( - (skill_path / "SKILL.md").read_text(encoding="utf-8") - ) - name = frontmatter.get("name") - description = frontmatter.get("description") - if isinstance(name, str) and name.strip(): - canonical_name = name.strip() - if isinstance(description, str) and description.strip(): - canonical_description = description.strip() - return extract_skill_editorial_metadata( - frontmatter, - fallback_name=canonical_name, - fallback_description=canonical_description, - ) - except (OSError, UnicodeError, ValueError): - return { - "editorial_name": canonical_name, - "editorial_description": canonical_description, - } - - -# ── File iteration ──────────────────────────────────────────────────────── - - def iter_skill_index_files(skills_dir: Path, filename: str): - """Walk skills_dir yielding sorted paths matching *filename*. - - Excludes Hermes metadata, VCS, virtualenv/dependency, cache, and skill - support directories. Support directories (references/templates/assets/ - scripts) can contain arbitrary markdown and even archived package - ``SKILL.md`` files, but they are progressive-disclosure data loaded through - ``skill_view(..., file_path=...)`` rather than active skill roots. - - M2 org mirrors (``_org/``) and Collective Wisdom installs - (``_wisdom/``): TOKEN-GATED resolution. Only the active org's - subdir (per the sync-client-written ``.active_org`` marker) is walked; - every other ``_org//`` (stale mirror from a previous org, or no - marker at all) is pruned — leave an org and its skills stop resolving, - without any manual cleanup. - """ + """Walk skills_dir yielding sorted paths matching *filename*; prunes + EXCLUDED_SKILL_DIRS and support dirs of skill roots. Org mirrors are + TOKEN-GATED: only the active org's subdir is walked, so leaving an org + stops its skills resolving without manual cleanup.""" skills_dir_str = str(skills_dir) active_org = read_active_org_id(skills_dir) - active_wisdom_org = read_active_wisdom_org_id(skills_dir) org_root = os.path.join(skills_dir_str, ORG_MIRROR_DIR_NAME) - wisdom_root = os.path.join(skills_dir_str, WISDOM_MANAGED_DIR_NAME) matches: list[str] = [] for root, dirs, files in os.walk(skills_dir_str, followlinks=True): has_skill_md = "SKILL.md" in files if root == skills_dir_str and ORG_MIRROR_DIR_NAME in dirs and active_org is None: dirs.remove(ORG_MIRROR_DIR_NAME) - if root == skills_dir_str and WISDOM_MANAGED_DIR_NAME in dirs and active_wisdom_org is None: - dirs.remove(WISDOM_MANAGED_DIR_NAME) elif root == org_root: dirs[:] = [d for d in dirs if d == active_org] - elif root == wisdom_root: - # Inside _wisdom/: descend ONLY into the last Gateway-verified org. - dirs[:] = [d for d in dirs if d == active_wisdom_org] - dirs[:] = [ - d - for d in dirs - if d not in EXCLUDED_SKILL_DIRS - and not (has_skill_md and d in SKILL_SUPPORT_DIRS) - ] + dirs[:] = [d for d in dirs if d not in EXCLUDED_SKILL_DIRS and not (has_skill_md and d in SKILL_SUPPORT_DIRS)] if filename in files: matches.append(os.path.join(root, filename)) yield from map(Path, sorted(matches)) diff --git a/apps/desktop/src/api/wisdom.ts b/apps/desktop/src/api/wisdom.ts deleted file mode 100644 index 890f684398..0000000000 --- a/apps/desktop/src/api/wisdom.ts +++ /dev/null @@ -1,576 +0,0 @@ -import type { WisdomMuteControl, WisdomMuteDuration, WisdomMuteSnapshot, WisdomSyncSnapshot } from '@hermes/shared' - -import type { ActionResponse } from '@/types/hermes' - -import { capabilityScoped, type ProfileScope } from './client' - -export type WisdomReviewStatus = 'advisory' | 'blocked' | 'pass' | 'pending' | 'retry' | 'running' | 'unavailable' - -export interface WisdomReviewCheckRow { - key: string - label?: string - status: WisdomReviewStatus - finding_count: number - details: string[] -} - -export interface WisdomReviewCheck { - source?: 'local_preflight' | string - schema_version?: number - status: WisdomReviewStatus - summary?: string - checks?: WisdomReviewCheckRow[] - provenance?: { kind: 'agent_assessed'; model: null | string; provider: null | string } -} - -export interface WisdomEntitlement { - entitled: boolean - org_id: null | string - scopes: string[] - expires_at: null | number -} - -export interface WisdomStatus { - configured: boolean - setup_required_reason?: 'not_configured' | 'organization_changed' | null - gateway_available: boolean - capability_advertised: boolean - verified_org_id: null | string - authenticated_org_id?: null | string - display_scopes: string[] - error?: null | string -} - -export interface WisdomCandidate { - local_skill_id: string - name: string - editorial_name?: string - editorial_description?: string - path: string - content_hash: string - eligibility: 'eligible' | 'instruction_only_fork_required' - reason: null | string - qualification: string - qualification_sequence: number | null - notice_variant: 'first' | 'returning' | null - organization_name: string | null - contribution_state: 'new' | 'prepared' - professionalism_check?: null | WisdomReviewCheck -} - -export interface WisdomCandidateEvent { - id: string - kind: 'wisdom.candidate' - session_id: null | string - task_id: null | string - content_hash: string - qualification_sequence: number - notice_variant: 'first' | 'returning' - organization_name: string | null - payload: { - skill_name: string - editorial_name?: string - editorial_description?: string - qualification: string - local_reasons: Record - consent_required: true - networked: false - } -} - -export interface WisdomSkillSummary { - id: string - slug: string - state: string - latest_version: null | number - author_description: null | string - install_count: number - scan_verdict?: null | string - system_spec?: null | Record - security_check?: null | WisdomReviewCheck - professionalism_check?: null | WisdomReviewCheck -} - -export interface WisdomDiscovery { - skills: WisdomSkillSummary[] - next_cursor: null | string -} - -export interface WisdomDraft { - id: string - slug: string - state: string - authorDescription: null | string - explanation?: null | string - scan?: null | Record - scanVerdict: null | string - systemSpec?: null | Record - updatedAt: string - security_check?: null | WisdomReviewCheck - professionalism_check?: null | WisdomReviewCheck -} - -export interface WisdomPreparedDraft { - hashes: WisdomDraftReview['hashes'] - network_submission: false - local_draft_id: string - overlay_path: string - drafted_description: string - files: WisdomDraftReview['files'] - local_scan: WisdomLocalScan - system_specification: Record - next_step: string - professionalism_check: WisdomReviewCheck -} - -export type WisdomCandidatePreparation = - | { - stage: 'prepared' - prepared: WisdomPreparedDraft - local_skill_id: string - skill_name: string - } - | { stage: 'review'; review: WisdomDraftReview } - -export interface WisdomLocalScan { - guard: Record - skill_evaluator: Record -} - -export interface WisdomSubmittedDraft { - draft: WisdomDraft - local_scan: WisdomLocalScan - notice: string - professionalism_check: WisdomReviewCheck -} - -export interface WisdomDraftReview { - draft: WisdomDraft & Record - effective_policy: Record - files: Array<{ content_utf8: string; hash: string; mode: 'exec' | 'file'; path: string }> - hashes: { author_description: string; content: string; package_manifest: string } - receipt: null | string -} - -export type WisdomPublicationReview = WisdomDraftReview & { - publication_mode: 'open' | 'managed' | 'moderated' - portal_url?: string -} -export interface WisdomPublicationResult { - draft_id: string - publication_state: 'pending_moderation' | 'published' - portal_url: string -} - -export const reviewWisdomPublication = (draftId: string, profile?: ProfileScope): Promise => - request('/api/wisdom/publication/review', profile, { - method: 'POST', - timeoutMs: 120_000, - body: { draft_id: draftId } - }) - -export const submitWisdomPublication = ( - review: WisdomPublicationReview, - profile?: ProfileScope, - consent?: { interaction_id: string; session_id: string } -): Promise => - request('/api/wisdom/publication/submit', profile, { - method: 'POST', - timeoutMs: 120_000, - body: { - draft_id: review.draft.id, - expected_hashes: review.hashes, - publication_mode: review.publication_mode, - ...consent - } - }) - -export const prepareWisdomConsentPublication = ( - interactionId: string, - sessionId: string, - profile?: ProfileScope -): Promise<{ draft_id: string }> => - request('/api/wisdom/consent/publication-review', profile, { - method: 'POST', - timeoutMs: 120_000, - body: { interaction_id: interactionId, session_id: sessionId, action: 'inspect' } - }) - -export interface WisdomEditedFile { - path: string - content_utf8: string -} - -export interface WisdomRevisedDraft { - draft: WisdomDraft & Record - local_scan: WisdomLocalScan - notice: string - professionalism_check: WisdomReviewCheck -} - -export interface WisdomSkillDetail { - latest_version_detail?: Record - local_compatibility?: Record - local_installation?: null | Record - portal_url?: string | null - skill: Record - versions: Array> -} - -export interface WisdomVersionDetail { - local_compatibility?: Record - local_installation?: null | Record - portal_url?: string | null - skill: Record - version: Record -} - -export interface WisdomVersionContent { - commit: string - content_hash: string - files: Array<{ content_utf8: string; hash: string; mode: 'exec' | 'file'; path: string }> -} - -export type WisdomUpdateMode = 'AUTO_WITH_NOTICE' | 'MANUAL' | 'REQUIRED' - -export interface WisdomManagedInstall { - skill_id: string - slug: string - version: number - update_mode: WisdomUpdateMode - state: string - target_path: string -} - -export type WisdomNotificationCategory = - 'installed' | 'new_skill' | 'publication_decision' | 'unavailable' | 'update_available' | 'updated' - -export interface WisdomNotification { - category: WisdomNotificationCategory - editorial_description?: string | null - editorial_name?: string | null - event_id: string - kind: string - moderation_note?: string | null - occurred_at?: string | null - portal_url?: string | null - skill_id: string - skill_name: string - source_event_ids: string[] - state?: string | null - version?: number | null -} - -export interface WisdomInstallations { - delivery_mode?: 'agent' | 'fixed' - installations: WisdomManagedInstall[] - notifications: WisdomNotification[] -} - -export type WisdomConsentAction = - 'inspect' | `inspect.${number}` | 'defer' | 'confirm' | 'recheck' | 'setup.status' | 'setup.recover' | 'setup.clear' - -export interface WisdomConsentInteraction { - id: string - assessment_id: string - state: string - operation: 'share' | 'install' | 'update' | 'publish' | 'setup' - expires_at: number - actions: ('defer' | 'inspect' | 'confirm')[] - deferred?: boolean - deferred_surfaces?: string[] - inspection?: { path: string; content: string; hash: string; description: string; page: number; page_count: number } - result?: { packaging_state?: 'queued' | 'ready' | 'failed' } - setup_review?: { - summary: string - detail: string - command: string - command_label: string - actions: { action: WisdomConsentAction; label: string; primary: boolean }[] - } - facts: { - step?: { phase: 'prerequisite' | 'setup' | 'verify'; index: number; command: string } - setup_instruction?: string - setup_explanation?: string - slug?: string - version?: number - editorial_name?: string | null - editorial_description?: string | null - compatibility?: { outcome: string } - modified?: boolean - sensitive_expansion?: string[] - security_check?: WisdomReviewCheck | null - professionalism_check?: WisdomReviewCheck | null - file_names?: string[] - } -} - -export interface WisdomMediationActivity { - mode: 'fixed' | 'agent' - assessments: { - id: string - state: string - owner_session: string | null - advice: null | { title: string; explanation: string; relevance: 'recommend' | 'digest' } - }[] - interactions: WisdomConsentInteraction[] -} - -export type WisdomInstallationCheckState = - 'archived' | 'current' | 'not_recorded' | 'taken_down' | 'update_available' | 'updated' - -export interface WisdomInstallationCheck { - skill_id: string - state: WisdomInstallationCheckState - plan?: WisdomActionPlan - result?: Record -} - -export interface WisdomCheckResult { - installations: WisdomInstallationCheck[] - qualification_events?: unknown[] - feed?: Record - owner_decisions?: Record - telegram?: Record -} - -export interface WisdomActionPlan { - receipt?: string - state?: string - skill_id: string - version?: number - compatibility?: { outcome: string; reasons?: string[] } - sensitive_expansion?: string[] - modified?: boolean - update_mode?: string - allowed?: boolean -} - -const request = ( - path: string, - profile?: ProfileScope, - init?: { body?: unknown; method?: string; timeoutMs?: number } -): Promise => - window.hermesDesktop.api({ - ...capabilityScoped(profile), - path, - method: init?.method, - timeoutMs: init?.timeoutMs, - body: init?.body - }) - -export const getWisdomStatus = (profile?: ProfileScope): Promise => request('/api/wisdom/status', profile) -export const getWisdomEntitlement = (profile?: ProfileScope): Promise => - request('/api/wisdom/entitlement', profile) - -export const getWisdomSync = (profile?: ProfileScope): Promise => - request('/api/wisdom/sync', profile) -export const retryWisdomSync = (profile?: ProfileScope): Promise => - request('/api/wisdom/sync/retry', profile, { method: 'POST', body: {}, timeoutMs: 120_000 }) - -export const getWisdomMute = (profile?: ProfileScope): Promise => - request('/api/wisdom/mute', profile) -export const prepareWisdomMute = (profile?: ProfileScope): Promise => - request('/api/wisdom/mute/prepare', profile, { method: 'POST', body: {} }) -export const chooseWisdomMute = ( - controlId: string, - duration: WisdomMuteDuration, - profile?: ProfileScope -): Promise => - request('/api/wisdom/mute/choose', profile, { method: 'POST', body: { control_id: controlId, duration } }) - -export const getWisdomMediation = (profile?: ProfileScope): Promise => - request('/api/wisdom/mediation', profile) - -export const resolveWisdomConsent = ( - interactionId: string, - sessionId: string, - action: WisdomConsentAction, - profile?: ProfileScope -): Promise => - request('/api/wisdom/consent', profile, { - method: 'POST', - body: { interaction_id: interactionId, session_id: sessionId, action } - }) - -export const setupWisdom = (profile?: ProfileScope): Promise => - request('/api/wisdom/setup', profile, { method: 'POST', body: { accept_disclosure: true } }) - -export const scanWisdom = (skill?: string, profile?: ProfileScope): Promise => - request('/api/wisdom/scan', profile, { method: 'POST', body: { skill } }) - -export const getWisdomCandidates = (profile?: ProfileScope): Promise<{ candidates: WisdomCandidate[] }> => - request('/api/wisdom/candidates', profile) - -export const getWisdomEvents = ( - sessionId: string, - profile?: ProfileScope -): Promise<{ events: WisdomCandidateEvent[] }> => - request(`/api/wisdom/events?session_id=${encodeURIComponent(sessionId)}`, profile) - -export const getWisdomDiscovery = (profile?: ProfileScope): Promise => - request('/api/wisdom/discovery', profile) - -export const getWisdomDrafts = (profile?: ProfileScope): Promise<{ drafts: WisdomDraft[] }> => - request('/api/wisdom/drafts', profile) - -export const getWisdomSkill = (skillId: string, profile?: ProfileScope): Promise => - request(`/api/wisdom/skills/${encodeURIComponent(skillId)}`, profile) - -export const getWisdomVersion = ( - skillId: string, - version: number, - profile?: ProfileScope -): Promise => - request(`/api/wisdom/skills/${encodeURIComponent(skillId)}/versions/${version}`, profile) - -export const getWisdomVersionContent = ( - skillId: string, - version: number, - profile?: ProfileScope -): Promise => - request(`/api/wisdom/skills/${encodeURIComponent(skillId)}/versions/${version}/content`, profile) - -export const getWisdomInstallations = (profile?: ProfileScope): Promise => - request('/api/wisdom/installations', profile) - -export const checkWisdom = (profile?: ProfileScope, applyAutomatic = true): Promise => - request('/api/wisdom/check', profile, { method: 'POST', body: { apply_automatic: applyAutomatic } }) - -export const planWisdomInstall = ( - reference: string, - profile?: ProfileScope, - updateMode?: WisdomUpdateMode -): Promise => - request('/api/wisdom/install/plan', profile, { - method: 'POST', - body: { reference, update_mode: updateMode } - }) - -export const applyWisdomInstall = ( - receipt: string, - acceptPartial: boolean, - profile?: ProfileScope -): Promise> => - request('/api/wisdom/install/apply', profile, { - method: 'POST', - body: { accept_partial: acceptPartial, receipt } - }) - -export const planWisdomUpdate = (skillId: string, profile?: ProfileScope): Promise => - request('/api/wisdom/update/plan', profile, { method: 'POST', body: { skill_id: skillId } }) - -export const applyWisdomUpdate = ( - receipt: string, - confirmations: { acceptPartial: boolean; acceptSensitive: boolean; preserveModified: boolean }, - profile?: ProfileScope -): Promise> => - request('/api/wisdom/update/apply', profile, { - method: 'POST', - body: { - accept_partial: confirmations.acceptPartial, - accept_sensitive: confirmations.acceptSensitive, - preserve_modified: confirmations.preserveModified, - receipt - } - }) - -export const uninstallWisdomSkill = (skillId: string, profile?: ProfileScope): Promise> => - request('/api/wisdom/uninstall', profile, { method: 'POST', body: { skill_id: skillId } }) - -export const acknowledgeWisdomNotifications = (profile?: ProfileScope): Promise<{ events: WisdomNotification[] }> => - request('/api/wisdom/notifications', profile, { method: 'POST', body: { mark_seen: true } }) - -export const suggestWisdomSkill = ( - skill: string, - profile?: ProfileScope, - approval?: { description: string; systemSpecification: Record }, - localSkillId?: string -): Promise => - request('/api/wisdom/suggest', profile, { - method: 'POST', - body: { - skill, - local_skill_id: localSkillId, - description: approval?.description, - system_specification: approval?.systemSpecification - } - }) - -export const reviewWisdomDraft = ( - draftId: string, - acknowledge: boolean, - profile?: ProfileScope -): Promise => - request('/api/wisdom/review', profile, { - method: 'POST', - body: { acknowledge, draft_id: draftId } - }) - -export const saveWisdomPreparedDraft = ( - draftId: string, - authorDescription: string, - files: WisdomEditedFile[], - profile?: ProfileScope -): Promise => - request('/api/wisdom/prepared/save', profile, { - method: 'POST', - body: { author_description: authorDescription, draft_id: draftId, files } - }) - -export const dismissWisdomCandidate = ( - localSkillId: string, - contentHash: string, - profile?: ProfileScope -): Promise<{ dismissed: true }> => - request('/api/wisdom/candidates/dismiss', profile, { - method: 'POST', - body: { content_hash: contentHash, local_skill_id: localSkillId } - }) - -export const deferWisdomCandidate = ( - eventId: string, - profile?: ProfileScope -): Promise<{ event_id: string; state: 'deferred' }> => - request('/api/wisdom/candidates/defer', profile, { - method: 'POST', - body: { event_id: eventId } - }) - -export const prepareWisdomCandidate = (eventId: string, profile?: ProfileScope): Promise => - request('/api/wisdom/candidates/prepare', profile, { - method: 'POST', - body: { event_id: eventId } - }) - -export const approveWisdomCandidate = (eventId: string, profile?: ProfileScope): Promise> => - request('/api/wisdom/candidates/approve', profile, { - method: 'POST', - body: { event_id: eventId } - }) - -export const reviseWisdomDraft = ( - draftId: string, - authorDescription: string, - files: WisdomEditedFile[], - hashes: WisdomDraftReview['hashes'], - profile?: ProfileScope -): Promise => - request('/api/wisdom/revise', profile, { - method: 'POST', - body: { - draft_id: draftId, - author_description: authorDescription, - files, - expected_content_hash: hashes.content, - expected_author_description_hash: hashes.author_description, - expected_package_manifest_hash: hashes.package_manifest - } - }) - -export const decideWisdomDraft = ( - draftId: string, - decision: 'approve' | 'decline', - profile?: ProfileScope -): Promise> => - request(`/api/wisdom/${decision}`, profile, { method: 'POST', body: { draft_id: draftId } }) diff --git a/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.test.tsx b/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.test.tsx index 1fa915b9d9..518090d96c 100644 --- a/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.test.tsx +++ b/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.test.tsx @@ -157,32 +157,6 @@ describe('useSlashCompletions', () => { expect(commandsOf(await completions(api, 'research'))).toEqual(['/research-paper-writing', '/research']) }) - it('shows backend documentation for wisdom subcommands', async () => { - const request = vi.fn().mockImplementation((method: string) => - Promise.resolve( - method === 'commands.catalog' - ? CATALOG - : { - replace_from: 8, - items: [ - { - text: 'installed', - display: 'installed', - meta: 'List and manage skills installed on this device' - } - ] - } - ) - ) - - const api = harness({ request } as unknown as HermesGateway) - const [installed] = await completions(api, 'wisdom ') - - expect(installed?.label).toBe('installed') - expect(installed?.description).toBe('List and manage skills installed on this device') - expect((installed?.metadata as { command?: string })?.command).toBe('/wisdom installed') - }) - it('keeps a registry command in Commands even when the desktop table has no row', async () => { const request = vi.fn().mockImplementation((method: string) => Promise.resolve( diff --git a/apps/desktop/src/app/chat/index.tsx b/apps/desktop/src/app/chat/index.tsx index c2786691aa..1b95bc769a 100644 --- a/apps/desktop/src/app/chat/index.tsx +++ b/apps/desktop/src/app/chat/index.tsx @@ -18,7 +18,7 @@ import { PromptOverlays } from '@/components/prompt-overlays' import { Button } from '@/components/ui/button' import { ErrorState } from '@/components/ui/error-state' import { TitleMenuTrigger } from '@/components/ui/title-menu-trigger' -import { type HermesGateway, type ProfileScope } from '@/hermes' +import { type HermesGateway } from '@/hermes' import { useI18n } from '@/i18n' import type { ChatMessage } from '@/lib/chat-messages' import { NEW_SESSION_TITLE, quickModelOptions, sessionTitle } from '@/lib/chat-runtime' @@ -28,7 +28,6 @@ import { useStoreSelector } from '@/lib/use-session-slice' import { cn } from '@/lib/utils' import { migrateSessionDraft } from '@/store/composer' import { migrateQueuedPrompts, parkQueuedPrompts } from '@/store/composer-queue' -import { activeGatewayConnectionId } from '@/store/gateway' import { $introSplash } from '@/store/intro-splash' import { $pinnedSessionIds } from '@/store/layout' import { $petActive } from '@/store/pet' @@ -429,13 +428,6 @@ const ChatViewContent = memo(function ChatViewContent({ const awaitingResponse = useStore(view.$awaitingResponse) const busy = useStore(view.$busy) const activeGatewayProfile = useStore($activeGatewayProfile) - const wisdomConnectionId = activeGatewayConnectionId() - - const wisdomProfile = useMemo( - () => ({ connectionId: wisdomConnectionId, profile: activeGatewayProfile }), - [activeGatewayProfile, wisdomConnectionId] - ) - const contextSuggestions = useStore($contextSuggestions) // Per-session (SessionView) reads — a tile IS its session, so these come // from the view slice, not the global atoms (which track the primary only). @@ -705,7 +697,6 @@ const ChatViewContent = memo(function ChatViewContent({ onRestoreToMessage={onRestoreToMessage} sessionId={activeSessionId} sessionKey={threadKey} - wisdomProfile={wisdomProfile} /> {resumeExhausted && routedSessionId && (
diff --git a/apps/desktop/src/app/skills/collective-tab.test.tsx b/apps/desktop/src/app/skills/collective-tab.test.tsx deleted file mode 100644 index 72b03f3ae4..0000000000 --- a/apps/desktop/src/app/skills/collective-tab.test.tsx +++ /dev/null @@ -1,621 +0,0 @@ -// @vitest-environment jsdom -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { fireEvent, render, screen, waitFor, within } from '@testing-library/react' -import { MemoryRouter } from 'react-router' -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' - -import type * as HermesApi from '@/hermes' - -const getWisdomStatus = vi.fn() -const getWisdomDiscovery = vi.fn() -const getWisdomCandidates = vi.fn() -const getWisdomDrafts = vi.fn() -const getWisdomSkill = vi.fn() -const getWisdomInstallations = vi.fn() -const getWisdomVersionContent = vi.fn() -const suggestWisdomSkill = vi.fn() -const reviewWisdomDraft = vi.fn() -const reviewWisdomPublication = vi.fn() -const saveWisdomPreparedDraft = vi.fn() -const submitWisdomPublication = vi.fn() -const reviseWisdomDraft = vi.fn() -const decideWisdomDraft = vi.fn() -const planWisdomInstall = vi.fn() -const applyWisdomInstall = vi.fn() -const planWisdomUpdate = vi.fn() -const applyWisdomUpdate = vi.fn() -const checkWisdom = vi.fn() -const setupWisdom = vi.fn() -const getActionStatus = vi.fn() - -vi.mock('@/hermes', async importOriginal => ({ - ...(await importOriginal()), - decideWisdomDraft, - getWisdomCandidates, - getWisdomDiscovery, - getWisdomDrafts, - getWisdomSkill, - getWisdomInstallations, - getWisdomVersionContent, - getWisdomStatus, - reviewWisdomDraft, - reviewWisdomPublication, - saveWisdomPreparedDraft, - submitWisdomPublication, - reviseWisdomDraft, - suggestWisdomSkill, - planWisdomInstall, - applyWisdomInstall, - planWisdomUpdate, - applyWisdomUpdate, - checkWisdom, - setupWisdom, - getActionStatus -})) - -vi.mock('@/store/notifications', () => ({ notifyError: vi.fn() })) - -const scope = { connectionId: 'gateway-a', profile: 'research' } -const originalScrollIntoView = Element.prototype.scrollIntoView - -beforeAll(() => { - Element.prototype.scrollIntoView = vi.fn() -}) - -afterAll(() => { - Element.prototype.scrollIntoView = originalScrollIntoView -}) - -beforeEach(() => { - checkWisdom.mockResolvedValue({ installations: [] }) -}) - -const systemSpecification = { - hermes: { minimum_version: '0.20.5' }, - platforms: ['macOS'], - architectures: ['arm64'], - model: { capabilities: [], minimum_context_window: null }, - tools: [], - plugins: [], - credentials: [], - connections: [], - filesystem: { read: [], write: [] }, - network: { destinations: [] }, - runtime: { shell: false, browser: false, code: false, sandbox: true }, - hardware: [], - known_limitations: [] -} - -async function renderTab(initialEntry = '/skills?tab=collective') { - const { CollectiveTab } = await import('./collective-tab') - const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }) - - return render( - - - - - - ) -} - -afterEach(() => vi.clearAllMocks()) - -function mockInstallations() { - getWisdomInstallations.mockResolvedValue({ installations: [], notifications: [] }) -} - -describe('CollectiveTab', () => { - it('keeps collective reads disabled until disclosure setup is accepted', async () => { - getWisdomStatus - .mockResolvedValueOnce({ configured: false, verified_org_id: null }) - .mockResolvedValueOnce({ configured: true, verified_org_id: 'org-1' }) - getWisdomDiscovery.mockResolvedValue({ next_cursor: null, skills: [] }) - getWisdomCandidates.mockResolvedValue({ candidates: [] }) - getWisdomDrafts.mockResolvedValue({ drafts: [] }) - mockInstallations() - setupWisdom.mockResolvedValue({ ok: true, name: 'wisdom-setup', pid: 1 }) - getActionStatus.mockResolvedValue({ - name: 'wisdom-setup', - running: false, - exit_code: 0, - pid: 1, - lines: [] - }) - - await renderTab() - expect(await screen.findByText(/Candidate qualification stays on this profile/)).toBeTruthy() - expect(getWisdomDiscovery).not.toHaveBeenCalled() - fireEvent.click(screen.getByRole('button', { name: /set up this profile/ })) - - await waitFor(() => expect(setupWisdom).toHaveBeenCalledWith(scope)) - expect(getActionStatus).toHaveBeenCalledWith('wisdom-setup', 80, scope) - }, 30_000) - - it('scopes reads to the selected connection/profile and renders hostile text as text', async () => { - mockInstallations() - getWisdomStatus.mockResolvedValue({ configured: true, verified_org_id: 'org-1' }) - getWisdomCandidates.mockResolvedValue({ candidates: [] }) - getWisdomDrafts.mockResolvedValue({ drafts: [] }) - getWisdomDiscovery.mockResolvedValue({ - next_cursor: null, - skills: [ - { - id: 'skill-1', - slug: '', - author_description: '', - install_count: 0, - latest_version: 1, - state: 'active' - } - ] - }) - - await renderTab() - - expect(await screen.findByText('')).toBeTruthy() - expect(globalThis.document.querySelector('script')).toBeNull() - expect(getWisdomDiscovery).toHaveBeenCalledWith(scope) - expect(getWisdomCandidates).toHaveBeenCalledWith(scope) - }) - - it('refreshes shared-skill discovery on demand', async () => { - mockInstallations() - getWisdomStatus.mockResolvedValue({ configured: true, verified_org_id: 'org-1' }) - getWisdomCandidates.mockResolvedValue({ candidates: [] }) - getWisdomDrafts.mockResolvedValue({ drafts: [] }) - getWisdomDiscovery - .mockResolvedValueOnce({ - next_cursor: null, - skills: [ - { - id: 'skill-1', - slug: 'existing-skill', - author_description: 'Already visible', - install_count: 0, - latest_version: 1, - state: 'active' - } - ] - }) - .mockResolvedValue({ - next_cursor: null, - skills: [ - { - id: 'skill-1', - slug: 'existing-skill', - author_description: 'Already visible', - install_count: 0, - latest_version: 1, - state: 'active' - }, - { - id: 'skill-2', - slug: 'newly-shared-skill', - author_description: 'Published after the screen opened', - install_count: 0, - latest_version: 1, - state: 'active' - } - ] - }) - - await renderTab() - expect(await screen.findByText('existing-skill')).toBeTruthy() - expect(screen.queryByText('newly-shared-skill')).toBeNull() - fireEvent.click(screen.getByRole('button', { name: 'Refresh shared skills' })) - - expect(await screen.findByText('newly-shared-skill')).toBeTruthy() - expect(getWisdomDiscovery).toHaveBeenCalledTimes(2) - }) - - it('refreshes registry discovery while checking managed updates', async () => { - mockInstallations() - checkWisdom.mockResolvedValue({ installations: [] }) - getWisdomStatus.mockResolvedValue({ configured: true, verified_org_id: 'org-1' }) - getWisdomCandidates.mockResolvedValue({ candidates: [] }) - getWisdomDrafts.mockResolvedValue({ drafts: [] }) - getWisdomDiscovery.mockResolvedValueOnce({ next_cursor: null, skills: [] }).mockResolvedValue({ - next_cursor: null, - skills: [ - { - id: 'skill-2', - slug: 'discovered-during-update-check', - author_description: 'Newly shared', - install_count: 0, - latest_version: 1, - state: 'active' - } - ] - }) - - await renderTab() - fireEvent.click(await screen.findByRole('button', { name: 'Check updates' })) - - expect(await screen.findByText('discovered-during-update-check')).toBeTruthy() - expect(checkWisdom).toHaveBeenCalledWith(scope) - expect(getWisdomDiscovery).toHaveBeenCalledTimes(2) - }) - - it('checks on load and marks installed skills with a pending target version', async () => { - getWisdomStatus.mockResolvedValue({ configured: true, verified_org_id: 'org-1' }) - getWisdomCandidates.mockResolvedValue({ candidates: [] }) - getWisdomDrafts.mockResolvedValue({ drafts: [] }) - getWisdomDiscovery.mockResolvedValue({ - next_cursor: null, - skills: [ - { - id: 'skill-1', - slug: 'gateway-pull-canary', - author_description: 'Managed canary', - install_count: 1, - latest_version: 2, - state: 'active' - } - ] - }) - getWisdomInstallations.mockResolvedValue({ - installations: [ - { - skill_id: 'skill-1', - slug: 'gateway-pull-canary', - version: 1, - update_mode: 'MANUAL', - state: 'active', - target_path: '/managed/gateway-pull-canary' - } - ], - notifications: [] - }) - checkWisdom.mockResolvedValue({ - installations: [ - { - skill_id: 'skill-1', - state: 'update_available', - plan: { skill_id: 'skill-1', version: 2, receipt: 'wup_1' } - } - ] - }) - getWisdomSkill.mockResolvedValue({ - skill: { id: 'skill-1', slug: 'gateway-pull-canary' }, - versions: [{ version: 2 }] - }) - getWisdomVersionContent.mockResolvedValue({ commit: 'sha256:commit', content_hash: 'sha256:content', files: [] }) - - await renderTab() - - await waitFor(() => expect(checkWisdom).toHaveBeenCalledWith(scope)) - expect(await screen.findByText('v2 update available')).toBeTruthy() - expect(screen.getByRole('button', { name: 'Check updates (1)' })).toBeTruthy() - - fireEvent.click(screen.getByRole('button', { name: /gateway-pull-canary/ })) - expect(await screen.findByRole('button', { name: 'Review update' })).toBeTruthy() - }) - - it.each(['open', 'moderated'] as const)( - 'reviews and rescans locally before one final %s submission', - async publicationMode => { - mockInstallations() - getWisdomStatus.mockResolvedValue({ configured: true, verified_org_id: 'org-1' }) - getWisdomDiscovery.mockResolvedValue({ next_cursor: null, skills: [] }) - getWisdomCandidates.mockResolvedValue({ - candidates: [ - { - local_skill_id: 'local-1', - name: 'candidate-skill', - eligibility: 'eligible', - reason: null, - qualification: 'manual_selection', - contribution_state: 'new' - } - ] - }) - getWisdomDrafts.mockResolvedValue({ drafts: [] }) - suggestWisdomSkill.mockResolvedValueOnce({ - network_submission: false, - local_draft_id: 'local:draft', - overlay_path: '/private/overlay', - drafted_description: 'Drafted copy', - system_specification: systemSpecification, - next_step: 'review' - }) - const manifest = JSON.stringify({ schema_version: 1, name: 'candidate-skill', requirements: systemSpecification }) - - const initialReview = { - draft: { id: 'local:draft', slug: 'candidate-skill', state: 'prepared', authorDescription: 'Drafted copy' }, - publication_mode: publicationMode, - effective_policy: {}, - files: [ - { path: 'SKILL.md', mode: 'file', hash: 'sha256:skill', content_utf8: '# Candidate\n' }, - { path: 'skill.manifest.json', mode: 'file', hash: 'sha256:manifest', content_utf8: manifest } - ], - hashes: { - content: 'sha256:content', - author_description: 'sha256:description', - package_manifest: 'sha256:manifest' - }, - receipt: null - } - - const rescannedReview = { - ...initialReview, - draft: { ...initialReview.draft, authorDescription: 'Approved owner copy' }, - hashes: { ...initialReview.hashes, author_description: 'sha256:revised' } - } - - reviewWisdomPublication.mockResolvedValueOnce(initialReview).mockResolvedValueOnce(rescannedReview) - saveWisdomPreparedDraft.mockResolvedValue({ local_draft_id: 'local:draft' }) - submitWisdomPublication.mockResolvedValue({ - draft_id: 'draft-1', - publication_state: publicationMode === 'open' ? 'published' : 'pending_moderation', - portal_url: 'https://portal.example/skill/draft-1' - }) - - await renderTab() - fireEvent.click(await screen.findByText('View all local skills (1)')) - fireEvent.click(await screen.findByRole('button', { name: 'Start contribution' })) - const description = await screen.findByLabelText('Owner-authored description') - fireEvent.change(description, { target: { value: 'Approved owner copy' } }) - const action = publicationMode === 'open' ? 'Publish to team' : 'Submit for approval' - expect(screen.getByRole('button', { name: action })).toHaveProperty('disabled', true) - expect(submitWisdomPublication).not.toHaveBeenCalled() - fireEvent.click(screen.getByRole('button', { name: 'Save changes & rescan' })) - await waitFor(() => expect(screen.getByRole('button', { name: action })).toHaveProperty('disabled', false)) - expect(saveWisdomPreparedDraft).toHaveBeenCalledWith( - 'local:draft', - 'Approved owner copy', - initialReview.files.map(({ path, content_utf8 }) => ({ path, content_utf8 })), - scope - ) - fireEvent.click(screen.getByRole('button', { name: action })) - await waitFor(() => - expect(submitWisdomPublication).toHaveBeenCalledExactlyOnceWith(rescannedReview, scope, undefined) - ) - expect(suggestWisdomSkill).toHaveBeenCalledTimes(1) - expect(suggestWisdomSkill.mock.calls[0][3]).toBe('local-1') - expect((await screen.findByRole('link', { name: 'View in Portal' })).getAttribute('href')).toBe( - 'https://portal.example/skill/draft-1' - ) - } - ) - - it('separates qualified suggestions, manual inventory, and submissions waiting on collective approval', async () => { - mockInstallations() - getWisdomStatus.mockResolvedValue({ configured: true, verified_org_id: 'org-1' }) - getWisdomDiscovery.mockResolvedValue({ next_cursor: null, skills: [] }) - getWisdomCandidates.mockResolvedValue({ - candidates: [ - { - local_skill_id: 'qualified-1', - name: 'qualified-skill', - editorial_name: 'Qualified Skill', - editorial_description: 'A useful skill ready for team review.', - eligibility: 'eligible', - reason: null, - qualification: 'high_usage', - contribution_state: 'new' - }, - { - local_skill_id: 'manual-1', - name: 'manual-only-skill', - eligibility: 'eligible', - reason: null, - qualification: 'manual_selection', - contribution_state: 'new' - } - ] - }) - getWisdomDrafts.mockResolvedValue({ - drafts: [ - { id: 'draft-ready', slug: 'needs-review', state: 'ready' }, - { id: 'draft-pending', slug: 'waiting-on-admin', state: 'pending_moderation' }, - { id: 'draft-published', slug: 'already-shared', state: 'published' }, - { id: 'draft-invalid', slug: 'old-revision', state: 'invalidated' } - ] - }) - - await renderTab() - - expect(await screen.findByText('needs-review')).toBeTruthy() - expect(screen.getByText('Ready for your review')).toBeTruthy() - expect(screen.getByText('waiting-on-admin')).toBeTruthy() - expect(screen.getByText('Waiting for collective administrator approval')).toBeTruthy() - expect( - screen.getByText('Drafts awaiting your review and submissions waiting for collective approval.') - ).toBeTruthy() - expect(screen.getByText('1 qualified suggestion')).toBeTruthy() - expect(screen.getByText('Qualified Skill')).toBeTruthy() - expect(screen.getByText('A useful skill ready for team review.')).toBeTruthy() - expect(screen.getAllByRole('button', { name: 'Start contribution' })).toHaveLength(1) - expect(screen.getByText('View all local skills (1)')).toBeTruthy() - expect(screen.queryByText('already-shared')).toBeNull() - expect(screen.queryByText('old-revision')).toBeNull() - }) - - it('edits owner copy and Markdown through a rescanned successor before approval', async () => { - mockInstallations() - getWisdomStatus.mockResolvedValue({ configured: true, verified_org_id: 'org-1' }) - getWisdomDiscovery.mockResolvedValue({ next_cursor: null, skills: [] }) - getWisdomCandidates.mockResolvedValue({ candidates: [] }) - getWisdomDrafts.mockResolvedValue({ - drafts: [{ id: 'draft-1', slug: 'editable-skill', state: 'ready', authorDescription: 'Original copy' }] - }) - const manifest = `${JSON.stringify({ schema_version: 1, name: 'editable-skill', requirements: systemSpecification })}\n` - - const initialReview = { - draft: { - id: 'draft-1', - slug: 'editable-skill', - state: 'ready', - authorDescription: 'Original copy', - scanVerdict: 'PASS' - }, - effective_policy: {}, - publication_mode: 'moderated', - files: [ - { path: 'SKILL.md', mode: 'file', hash: 'sha256:skill', content_utf8: '# Original\n' }, - { path: 'skill.manifest.json', mode: 'file', hash: 'sha256:manifest', content_utf8: manifest } - ], - hashes: { - content: 'sha256:content', - author_description: 'sha256:description', - package_manifest: 'sha256:manifest' - }, - receipt: null - } - - const revisedReview = { - ...initialReview, - draft: { ...initialReview.draft, id: 'draft-2', authorDescription: 'Revised copy' }, - files: [{ ...initialReview.files[0], content_utf8: '# Revised\n' }, initialReview.files[1]], - hashes: { ...initialReview.hashes, content: 'sha256:revised' } - } - - reviewWisdomPublication.mockResolvedValueOnce(initialReview).mockResolvedValueOnce(revisedReview) - reviseWisdomDraft.mockResolvedValue({ draft: revisedReview.draft, local_scan: {}, notice: 'rescanned' }) - - await renderTab() - fireEvent.click(await screen.findByRole('button', { name: /editable-skill.*View details/ })) - fireEvent.change(await screen.findByLabelText('Owner-authored description'), { - target: { value: 'Revised copy' } - }) - fireEvent.change(screen.getByLabelText('Edit SKILL.md'), { target: { value: '# Revised\n' } }) - expect(screen.getByRole('button', { name: 'Submit for approval' })).toHaveProperty('disabled', true) - fireEvent.click(screen.getByRole('button', { name: 'Save changes & rescan' })) - - await waitFor(() => expect(reviseWisdomDraft).toHaveBeenCalledTimes(1)) - expect(reviseWisdomDraft).toHaveBeenCalledWith( - 'draft-1', - 'Revised copy', - [ - { path: 'SKILL.md', content_utf8: '# Revised\n' }, - { path: 'skill.manifest.json', content_utf8: manifest } - ], - initialReview.hashes, - scope - ) - await waitFor(() => - expect(screen.getByRole('button', { name: 'Submit for approval' })).toHaveProperty('disabled', false) - ) - }) - - it('requires a verified plan before applying a managed install', async () => { - mockInstallations() - getWisdomStatus.mockResolvedValue({ configured: true, verified_org_id: 'org-1' }) - getWisdomCandidates.mockResolvedValue({ candidates: [] }) - getWisdomDrafts.mockResolvedValue({ drafts: [] }) - getWisdomDiscovery.mockResolvedValue({ - next_cursor: null, - skills: [ - { - id: 'skill-1', - slug: 'managed-skill', - author_description: 'Does work', - install_count: 0, - latest_version: 2, - state: 'active' - } - ] - }) - getWisdomSkill.mockResolvedValue({ skill: { id: 'skill-1', slug: 'managed-skill' }, versions: [{ version: 2 }] }) - getWisdomVersionContent.mockResolvedValue({ commit: 'sha256:commit', content_hash: 'sha256:content', files: [] }) - planWisdomInstall - .mockResolvedValueOnce({ - receipt: 'wip_1', - skill_id: 'skill-1', - version: 2, - compatibility: { outcome: 'compatible' } - }) - .mockResolvedValueOnce({ - receipt: 'wip_auto', - skill_id: 'skill-1', - version: 2, - update_mode: 'AUTO_WITH_NOTICE', - compatibility: { outcome: 'compatible' } - }) - applyWisdomInstall.mockResolvedValue({ installed: true }) - - await renderTab() - fireEvent.click(await screen.findByRole('button', { name: /managed-skill/ })) - fireEvent.click(await screen.findByRole('button', { name: 'Install…' })) - const preview = await screen.findByRole('region', { name: 'Confirm install' }) - const dialog = screen.getByRole('dialog', { name: 'Verified managed action plan' }) - - expect(preview.textContent).toContain('wip_1') - fireEvent.click(within(dialog).getByRole('combobox', { name: 'Future updates' })) - fireEvent.click(await screen.findByRole('option', { name: 'Automatic with notice' })) - await waitFor(() => expect(planWisdomInstall).toHaveBeenLastCalledWith('skill-1', scope, 'AUTO_WITH_NOTICE')) - expect(preview.textContent).toContain('wip_auto') - - const confirm = screen.getByRole('button', { name: 'Confirm install' }) - expect(preview.contains(confirm)).toBe(false) - fireEvent.click(confirm) - await waitFor(() => expect(applyWisdomInstall).toHaveBeenCalledWith('wip_auto', false, scope)) - }) - - it('opens the verified update plan from a notification deep link', async () => { - mockInstallations() - getWisdomStatus.mockResolvedValue({ configured: true, verified_org_id: 'org-1' }) - getWisdomCandidates.mockResolvedValue({ candidates: [] }) - getWisdomDrafts.mockResolvedValue({ drafts: [] }) - getWisdomDiscovery.mockResolvedValue({ next_cursor: null, skills: [] }) - getWisdomSkill.mockResolvedValue({ - skill: { id: 'skill-1', slug: 'managed-skill' }, - versions: [{ version: 3 }] - }) - getWisdomVersionContent.mockResolvedValue({ commit: 'sha256:commit', content_hash: 'sha256:content', files: [] }) - planWisdomUpdate.mockResolvedValue({ - receipt: 'wup_notification', - skill_id: 'skill-1', - version: 3, - compatibility: { outcome: 'compatible' } - }) - - await renderTab('/skills?tab=collective&wisdomAction=update&wisdomSkillId=skill-1') - - await waitFor(() => expect(planWisdomUpdate).toHaveBeenCalledWith('skill-1', scope)) - expect((await screen.findByRole('region', { name: 'Confirm update' })).textContent).toContain('wup_notification') - expect(applyWisdomUpdate).not.toHaveBeenCalled() - }) - - it('plans a pasted Portal install link before allowing an install', async () => { - const portalLink = - 'http://127.0.0.1:3111/orgs/wisdom-local/wisdom/skills/0a192cc7-486e-426d-a6b4-493119c1c011?version=1' - - mockInstallations() - getWisdomStatus.mockResolvedValue({ configured: true, verified_org_id: 'org-1' }) - getWisdomCandidates.mockResolvedValue({ candidates: [] }) - getWisdomDrafts.mockResolvedValue({ drafts: [] }) - getWisdomDiscovery.mockResolvedValue({ next_cursor: null, skills: [] }) - getWisdomSkill.mockResolvedValue({ - skill: { id: '0a192cc7-486e-426d-a6b4-493119c1c011', slug: 'gateway-pull-canary' }, - versions: [] - }) - planWisdomInstall.mockResolvedValue({ - receipt: 'wip_from_link', - skill_id: '0a192cc7-486e-426d-a6b4-493119c1c011', - version: 1, - update_mode: 'AUTO_WITH_NOTICE', - compatibility: { outcome: 'compatible' } - }) - applyWisdomInstall.mockResolvedValue({ installed: true }) - - await renderTab() - fireEvent.change(await screen.findByLabelText('Install from link or skill ID'), { - target: { value: portalLink } - }) - fireEvent.click(screen.getByRole('combobox', { name: 'Future updates' })) - fireEvent.click(await screen.findByRole('option', { name: 'Automatic with notice' })) - fireEvent.click(screen.getByRole('button', { name: 'Review install' })) - - await waitFor(() => expect(planWisdomInstall).toHaveBeenCalledWith(portalLink, scope, 'AUTO_WITH_NOTICE')) - expect(applyWisdomInstall).not.toHaveBeenCalled() - expect(await screen.findByText(/wip_from_link/)).toBeTruthy() - const dialog = screen.getByRole('dialog', { name: 'Verified managed action plan' }) - expect(within(dialog).getByRole('combobox', { name: 'Future updates' }).textContent).toContain( - 'Automatic with notice' - ) - fireEvent.click(screen.getByRole('button', { name: 'Confirm install' })) - await waitFor(() => expect(applyWisdomInstall).toHaveBeenCalledWith('wip_from_link', false, scope)) - }) -}) diff --git a/apps/desktop/src/app/skills/collective-tab.tsx b/apps/desktop/src/app/skills/collective-tab.tsx deleted file mode 100644 index b9ace5fd25..0000000000 --- a/apps/desktop/src/app/skills/collective-tab.tsx +++ /dev/null @@ -1,1017 +0,0 @@ -import { useQuery } from '@tanstack/react-query' -import { useCallback, useEffect, useMemo, useState } from 'react' -import { useLocation, useNavigate } from 'react-router' - -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import { WisdomCheckBadge, WisdomReviewTables } from '@/components/wisdom-checks' -import { WisdomMediationCard } from '@/components/wisdom-mediation-card' -import { WisdomNotificationSettings } from '@/components/wisdom-notification-settings' -import { WisdomNotificationsCard } from '@/components/wisdom-notifications-card' -import { WisdomPublicationReview } from '@/components/wisdom-publication-review' -import { WisdomSyncStatus } from '@/components/wisdom-sync-status' -import { - acknowledgeWisdomNotifications, - applyWisdomInstall, - applyWisdomUpdate, - checkWisdom, - getActionStatus, - getWisdomCandidates, - getWisdomDiscovery, - getWisdomDrafts, - getWisdomInstallations, - getWisdomSkill, - getWisdomStatus, - getWisdomVersionContent, - planWisdomInstall, - planWisdomUpdate, - type ProfileScope, - profileScopeKey, - scanWisdom, - setupWisdom, - suggestWisdomSkill, - uninstallWisdomSkill, - type WisdomActionPlan, - type WisdomCandidate, - type WisdomCheckResult, - type WisdomReviewCheck, - type WisdomUpdateMode -} from '@/hermes' -import { useI18n } from '@/i18n' -import { cn } from '@/lib/utils' -import { notifyError } from '@/store/notifications' - -import { DetailColumn, ListColumn, ListStrip, MasterDetail } from '../master-detail' - -const TERMINAL_DRAFT_STATES = new Set(['published', 'declined', 'invalidated', 'rejected']) -const UPDATE_CHECK_INTERVAL_MS = 5 * 60 * 1000 - -function asRecord(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record) : {} -} - -const candidateDisplayName = (candidate: WisdomCandidate): string => candidate.editorial_name?.trim() || candidate.name - -const candidateDisplayDescription = (candidate: WisdomCandidate): string => - candidate.editorial_description?.trim() || '' - -async function waitForWisdomAction(name: string, profile: ProfileScope): Promise { - for (let attempt = 0; attempt < 1200; attempt += 1) { - const status = await getActionStatus(name, 80, profile) - - if (!status.running) { - if (status.exit_code !== 0) { - throw new Error(status.lines.at(-1) || `Collective Wisdom action failed (${status.exit_code ?? 'unknown'})`) - } - - return - } - - await new Promise(resolve => setTimeout(resolve, 500)) - } - - throw new Error('Collective Wisdom action timed out') -} - -export function CollectiveTab({ profile, query }: { profile: ProfileScope; query: string }) { - const { t } = useI18n() - const copy = t.skills.collective - const location = useLocation() - const navigate = useNavigate() - const scope = profileScopeKey(profile) - const [selectedId, setSelectedId] = useState(null) - - const [publicationDraftId, setPublicationDraftId] = useState(null) - const [busy, setBusy] = useState(null) - const [showManualCandidates, setShowManualCandidates] = useState(false) - const [installReference, setInstallReference] = useState('') - const [installUpdateMode, setInstallUpdateMode] = useState<'' | WisdomUpdateMode>('') - - const [actionPlan, setActionPlan] = useState< - null | (WisdomActionPlan & { action: 'install' | 'uninstall' | 'update' }) - >(null) - - const [actionPlanReference, setActionPlanReference] = useState(null) - - const [acceptSensitive, setAcceptSensitive] = useState(false) - const [acceptPartial, setAcceptPartial] = useState(false) - const [preserveModified, setPreserveModified] = useState(false) - - useEffect(() => { - setSelectedId(null) - setPublicationDraftId(null) - setShowManualCandidates(false) - setInstallReference('') - setInstallUpdateMode('') - setActionPlan(null) - setActionPlanReference(null) - setAcceptSensitive(false) - setAcceptPartial(false) - setPreserveModified(false) - setBusy(null) - }, [scope]) - - const status = useQuery({ - queryKey: ['wisdom-status', scope], - queryFn: () => getWisdomStatus(profile), - staleTime: 30_000 - }) - - const discovery = useQuery({ - queryKey: ['wisdom-discovery', scope], - queryFn: () => getWisdomDiscovery(profile), - staleTime: 30_000, - enabled: status.data?.configured === true - }) - - const candidates = useQuery({ - queryKey: ['wisdom-candidates', scope], - queryFn: () => getWisdomCandidates(profile), - staleTime: 15_000, - enabled: status.data?.configured === true - }) - - const drafts = useQuery({ - queryKey: ['wisdom-drafts', scope], - queryFn: () => getWisdomDrafts(profile), - staleTime: 15_000, - enabled: status.data?.configured === true - }) - - const detail = useQuery({ - queryKey: ['wisdom-detail', scope, selectedId], - queryFn: () => getWisdomSkill(selectedId || '', profile), - enabled: status.data?.configured === true && Boolean(selectedId), - staleTime: 30_000 - }) - - const installations = useQuery({ - queryKey: ['wisdom-installations', scope], - queryFn: () => getWisdomInstallations(profile), - staleTime: 10_000, - enabled: status.data?.configured === true - }) - - const refetchInstallations = installations.refetch - - const updateCheck = useQuery({ - queryKey: ['wisdom-update-check', scope], - queryFn: () => checkWisdom(profile), - enabled: status.data?.configured === true, - staleTime: UPDATE_CHECK_INTERVAL_MS, - refetchInterval: UPDATE_CHECK_INTERVAL_MS, - refetchIntervalInBackground: false, - refetchOnWindowFocus: true - }) - - useEffect(() => { - if (!updateCheck.dataUpdatedAt) { - return - } - - void refetchInstallations().catch(error => notifyError(error, 'Wisdom installation refresh failed')) - }, [refetchInstallations, updateCheck.dataUpdatedAt]) - - const latestSelectedVersion = useMemo( - () => Math.max(0, ...(detail.data?.versions ?? []).map(version => Number(version.version) || 0)), - [detail.data?.versions] - ) - - const content = useQuery({ - queryKey: ['wisdom-content', scope, selectedId, latestSelectedVersion], - queryFn: () => getWisdomVersionContent(selectedId || '', latestSelectedVersion, profile), - enabled: Boolean(selectedId && latestSelectedVersion), - staleTime: 60_000 - }) - - const rows = useMemo(() => { - const needle = query.trim().toLocaleLowerCase() - - return (discovery.data?.skills ?? []).filter( - skill => - !needle || - skill.slug.toLocaleLowerCase().includes(needle) || - (skill.author_description ?? '').toLocaleLowerCase().includes(needle) - ) - }, [discovery.data?.skills, query]) - - const pendingUpdates = useMemo( - () => - new Map( - (updateCheck.data?.installations ?? []) - .filter(item => item.state === 'update_available') - .map(item => [item.skill_id, item] as const) - ), - [updateCheck.data?.installations] - ) - - const activeDrafts = useMemo( - () => (drafts.data?.drafts ?? []).filter(draft => !TERMINAL_DRAFT_STATES.has(draft.state)), - [drafts.data?.drafts] - ) - - const { manualCandidates, qualifiedCandidates } = useMemo(() => { - const all = candidates.data?.candidates ?? [] - - return { - manualCandidates: all.filter(candidate => candidate.qualification === 'manual_selection'), - qualifiedCandidates: all.filter(candidate => candidate.qualification !== 'manual_selection') - } - }, [candidates.data?.candidates]) - - const filterCandidates = useCallback( - (items: WisdomCandidate[]) => { - const needle = query.trim().toLocaleLowerCase() - - return items - .filter(candidate => - !needle - ? true - : [candidate.name, candidateDisplayName(candidate), candidateDisplayDescription(candidate)].some(value => - value.toLocaleLowerCase().includes(needle) - ) - ) - .toSorted((left, right) => candidateDisplayName(left).localeCompare(candidateDisplayName(right))) - }, - [query] - ) - - const visibleQualifiedCandidates = useMemo( - () => filterCandidates(qualifiedCandidates), - [filterCandidates, qualifiedCandidates] - ) - - const visibleManualCandidates = useMemo( - () => filterCandidates(manualCandidates), - [filterCandidates, manualCandidates] - ) - - const refreshContributionData = useCallback(async () => { - await Promise.all([candidates.refetch(), drafts.refetch(), discovery.refetch()]) - }, [candidates, discovery, drafts]) - - const candidateSummary = (candidate: WisdomCandidate): string => { - if (candidate.eligibility !== 'eligible') { - return candidate.reason || copy.localOnly - } - - const qualification = - candidate.qualification === 'manual_selection' - ? copy.localOnly - : candidate.notice_variant === 'first' - ? copy.qualificationFirst(candidate.organization_name) - : copy.qualificationReturning - - return candidate.contribution_state === 'prepared' ? `${qualification} ${copy.savedLocally}` : qualification - } - - const prepare = async (candidate: WisdomCandidate) => { - setBusy(candidate.local_skill_id) - - try { - const result = await suggestWisdomSkill(candidate.name, profile, undefined, candidate.local_skill_id) - setPublicationDraftId('network_submission' in result ? result.local_draft_id : result.draft.id) - } catch (error) { - notifyError(error, 'Collective Wisdom preparation failed') - } finally { - setBusy(null) - } - } - - const openReview = (draftId: string) => setPublicationDraftId(draftId) - - const installed = installations.data?.installations.find( - item => item.skill_id === selectedId && item.state === 'active' - ) - - const selectedUpdate = selectedId ? pendingUpdates.get(selectedId) : undefined - - const planManagedActionForSkill = useCallback( - async (skillId: string, action: 'install' | 'uninstall' | 'update') => { - setSelectedId(skillId) - setBusy(skillId) - - try { - const plan = - action === 'install' - ? await planWisdomInstall(skillId, profile, installUpdateMode || undefined) - : action === 'update' - ? await planWisdomUpdate(skillId, profile) - : { skill_id: skillId, state: 'confirm_uninstall' } - - setActionPlan({ ...plan, action }) - setActionPlanReference(action === 'install' ? skillId : null) - setAcceptSensitive(false) - setAcceptPartial(false) - setPreserveModified(false) - } catch (error) { - notifyError(error, `Wisdom ${action} planning failed`) - } finally { - setBusy(null) - } - }, - [installUpdateMode, profile] - ) - - const planManagedAction = async (action: 'install' | 'uninstall' | 'update') => { - if (selectedId) { - await planManagedActionForSkill(selectedId, action) - } - } - - useEffect(() => { - if (!status.data?.configured) { - return - } - - const params = new URLSearchParams(location.search) - const action = params.get('wisdomAction') - const skillId = params.get('wisdomSkillId') - - if ((action !== 'install' && action !== 'update') || !skillId) { - return - } - - params.delete('wisdomAction') - params.delete('wisdomSkillId') - const search = params.toString() - - navigate( - { hash: location.hash, pathname: location.pathname, search: search ? `?${search}` : '' }, - { replace: true } - ) - void planManagedActionForSkill(skillId, action) - }, [location.hash, location.pathname, location.search, navigate, planManagedActionForSkill, status.data?.configured]) - - const planReferencedInstall = async () => { - const reference = installReference.trim() - - if (!reference) { - return - } - - setBusy('install-reference') - - try { - const plan = await planWisdomInstall(reference, profile, installUpdateMode || undefined) - setSelectedId(plan.skill_id) - setActionPlan({ ...plan, action: 'install' }) - setActionPlanReference(reference) - setAcceptSensitive(false) - setAcceptPartial(false) - setPreserveModified(false) - } catch (error) { - notifyError(error, 'Wisdom install planning failed') - } finally { - setBusy(null) - } - } - - const replanInstallUpdateMode = async (value: string) => { - if (!actionPlan || actionPlan.action !== 'install') { - return - } - - const previousMode = installUpdateMode - const nextMode = value === 'DEFAULT' ? '' : (value as WisdomUpdateMode) - const reference = actionPlanReference || actionPlan.skill_id - - setInstallUpdateMode(nextMode) - setBusy('install-mode') - - try { - const plan = await planWisdomInstall(reference, profile, nextMode || undefined) - setActionPlan({ ...plan, action: 'install' }) - setAcceptSensitive(false) - setAcceptPartial(false) - setPreserveModified(false) - } catch (error) { - setInstallUpdateMode(previousMode) - notifyError(error, 'Wisdom install planning failed') - } finally { - setBusy(null) - } - } - - const applyManagedAction = async () => { - if (!actionPlan) { - return - } - - setBusy(actionPlan.skill_id) - - try { - if (actionPlan.action === 'uninstall') { - await uninstallWisdomSkill(actionPlan.skill_id, profile) - } else if (!actionPlan.receipt) { - throw new Error('Verified action receipt is missing') - } else if (actionPlan.action === 'install') { - await applyWisdomInstall(actionPlan.receipt, acceptPartial, profile) - } else { - await applyWisdomUpdate(actionPlan.receipt, { acceptPartial, acceptSensitive, preserveModified }, profile) - } - - setActionPlan(null) - setActionPlanReference(null) - - if (actionPlan.action === 'install') { - setInstallReference('') - setInstallUpdateMode('') - } - - await Promise.all([installations.refetch(), discovery.refetch(), detail.refetch(), updateCheck.refetch()]) - } catch (error) { - notifyError(error, `Wisdom ${actionPlan.action} failed`) - } finally { - setBusy(null) - } - } - - const setupProfile = async () => { - setBusy('setup') - - try { - const action = await setupWisdom(profile) - await waitForWisdomAction(action.name, profile) - await status.refetch() - await Promise.all([discovery.refetch(), candidates.refetch(), drafts.refetch(), installations.refetch()]) - } catch (error) { - notifyError(error, 'Collective Wisdom setup failed') - } finally { - setBusy(null) - } - } - - if (status.isPending) { - return
{copy.loading}
- } - - if (status.isError) { - const error = status.error - - return ( -
- {copy.unavailable} {error instanceof Error ? error.message : ''} -
- ) - } - - if (!status.data.configured) { - return ( -
-
-
-

{copy.title}

-

{copy.setup}

-
-

{copy.setupDisclosure}

- {status.data.error && ( -
- {status.data.error} -
- )} - -
-
- ) - } - - if (discovery.isPending || candidates.isPending || drafts.isPending || installations.isPending) { - return
{copy.loading}
- } - - if (discovery.isError || candidates.isError || drafts.isError || installations.isError) { - const error = discovery.error || candidates.error || drafts.error || installations.error - - return ( -
- {copy.unavailable} {error instanceof Error ? error.message : ''} -
- ) - } - - const statusCopy = status.data?.verified_org_id ? `${status.data.verified_org_id} · ${copy.orgWide}` : copy.setup - - return ( -
-
- -
-
-
{copy.title}
-
{statusCopy}
-
-
- - - -
-
-
{ - event.preventDefault() - void planReferencedInstall() - }} - > -
- - setInstallReference(event.target.value)} - placeholder={copy.installReferencePlaceholder} - size="sm" - value={installReference} - /> -

- {copy.installReferenceHelp} -

-
-
- - -

- {copy.updateModeHelp} -

-
- -
- - - { - try { - await acknowledgeWisdomNotifications(profile) - await installations.refetch() - } catch (error) { - notifyError(error, 'Could not acknowledge Wisdom notifications') - } - }} - onPlanAction={(action, event) => planManagedActionForSkill(event.skill_id, action)} - /> -
- - {copy.sharedSkills(rows.length)}} - right={ - qualifiedCandidates.length > 0 ? ( - - {copy.localCandidates(qualifiedCandidates.length)} - - ) : undefined - } - /> - } - > - {rows.map(skill => ( - - ))} - {rows.length === 0 && ( -
{copy.noShared}
- )} -
- -
- {((candidates.data?.candidates.length ?? 0) > 0 || activeDrafts.length > 0) && ( -
-
-

{copy.potential}

-

{copy.potentialHelp}

-
- {visibleQualifiedCandidates.map(candidate => ( -
-
-
{candidateDisplayName(candidate)}
- {candidateDisplayDescription(candidate) && ( -
- {candidateDisplayDescription(candidate)} -
- )} -
- {candidateSummary(candidate)} -
- {candidate.professionalism_check && ( -
- -
- )} -
- -
- ))} - {visibleQualifiedCandidates.length === 0 && ( -

{copy.noSuggestions}

- )} - {manualCandidates.length > 0 && ( -
- - {showManualCandidates && ( - <> -

- {copy.browseLocalHelp} -

-
- {visibleManualCandidates.map(candidate => ( -
-
-
- {candidateDisplayName(candidate)} -
- {candidateDisplayDescription(candidate) && ( -
- {candidateDisplayDescription(candidate)} -
- )} -
- {candidateSummary(candidate)} -
- {candidate.professionalism_check && ( -
- -
- )} -
- -
- ))} -
- - )} -
- )} -
-
- -
-

{copy.ownerReview}

-

{copy.ownerReviewHelp}

-
- {activeDrafts.length === 0 ? ( -

{copy.noDrafts}

- ) : ( - activeDrafts.map(draft => ( - - )) - )} -
-
-
- )} - - {detail.data && ( -
-

{String(detail.data.skill.slug || detail.data.skill.id)}

-

- {String(detail.data.skill.authorDescription || detail.data.skill.author_description || '')} -

- -

{copy.versionHistory}

-
-                  {JSON.stringify(
-                    {
-                      latest_version: detail.data.latest_version_detail,
-                      version_history: detail.data.versions,
-                      local_compatibility: detail.data.local_compatibility
-                    },
-                    null,
-                    2
-                  )}
-                
- {content.data && ( -
-
content {content.data.content_hash}
- {content.data.files.map(file => ( -
- - {file.path} · {file.hash} - -
-                          {file.content_utf8}
-                        
-
- ))} -
- )} -
- {installed ? ( - <> - - {copy.installed(installed.version, installed.update_mode)} - - {selectedUpdate && ( - - {copy.updateAvailable(selectedUpdate.plan?.version)} - - )} - - - - ) : ( - - )} -
-
- )} -
-
-
- - {publicationDraftId && ( -
- { - setPublicationDraftId(null) - void refreshContributionData() - }} - onSubmitted={() => void refreshContributionData()} - profile={profile} - /> -
- )} - - {actionPlan && ( -
-

{copy.confirmAction(actionPlan.action)}

-
-
{JSON.stringify(actionPlan, null, 2)}
-
-
- {actionPlan.action === 'install' && ( -
- - -

- {copy.updateModeHelp} -

-
- )} - {actionPlan.state === 'current' &&

{copy.alreadyCurrent}

} - {actionPlan.compatibility && actionPlan.compatibility.outcome !== 'compatible' && ( - - )} - {(actionPlan.sensitive_expansion?.length ?? 0) > 0 && ( - - )} - {actionPlan.modified && actionPlan.update_mode !== 'REQUIRED' && ( - - )} -
- - {actionPlan.state !== 'current' && ( - - )} -
-
-
- )} -
- ) -} diff --git a/apps/desktop/src/app/skills/index.test.tsx b/apps/desktop/src/app/skills/index.test.tsx index 5f8174cf1d..b4647437dd 100644 --- a/apps/desktop/src/app/skills/index.test.tsx +++ b/apps/desktop/src/app/skills/index.test.tsx @@ -19,7 +19,6 @@ const getUsageAnalytics = vi.fn() const getProfiles = vi.fn() const getSkillContent = vi.fn() const getOfficialSkills = vi.fn() -const getWisdomEntitlement = vi.fn() // Partial mock: keep the real module (SkillsView pulls in @/store/profile, // whose import-time subscription calls setApiRequestProfile) and stub only the @@ -37,8 +36,7 @@ vi.mock('@/hermes', async importOriginal => ({ getUsageAnalytics: (days: number, profile?: null | string) => getUsageAnalytics(days, profile), getProfiles: () => getProfiles(), getSkillContent: (name: string, profile?: null | string) => getSkillContent(name, profile), - getOfficialSkills: (profile?: null | string) => getOfficialSkills(profile), - getWisdomEntitlement: (profile?: null | string) => getWisdomEntitlement(profile) + getOfficialSkills: (profile?: null | string) => getOfficialSkills(profile) })) // Notifications hit nanostores/timers we don't care about here. @@ -47,15 +45,6 @@ vi.mock('@/store/notifications', () => ({ notifyError: vi.fn() })) -// Tab contents have their own suites; this suite owns route-to-panel selection. -vi.mock('@/components/chat/code-editor', () => ({ CodeEditor: () => null })) -vi.mock('./collective-tab', () => ({ - CollectiveTab: () =>
-})) -vi.mock('./plugins-tab', () => ({ - PluginsTab: () =>
-})) - // The catalog Install button routes through the hub action pipeline — stub the // action entrypoint (real module kept: SkillsView reads $hubActions and the // query keys from it). @@ -86,26 +75,20 @@ function toolset(overrides: Record = {}) { } } -async function renderSkills(tab = 'toolsets') { +async function renderSkills() { const { SkillsView } = await import('./index') let result: ReturnType await act(async () => { result = render( // SkillsView reads skills/toolsets via useQuery, so it needs a provider. - + ) }) - if (vi.isFakeTimers()) { - await act(async () => { - await vi.advanceTimersByTimeAsync(50) - }) - } - return result! } @@ -116,12 +99,6 @@ beforeEach(() => { getToolsetConfig.mockResolvedValue({ has_category: true, active_provider: null, providers: [] }) getUsageAnalytics.mockResolvedValue({ tools: [] }) getOfficialSkills.mockResolvedValue({ skills: [] }) - getWisdomEntitlement.mockResolvedValue({ - entitled: true, - org_id: 'org-1', - scopes: ['wisdom:read'], - expires_at: Date.now() / 1000 + 60 - }) getSkillContent.mockResolvedValue({ name: 'web-research', path: '/skills/web-research/SKILL.md', @@ -134,7 +111,6 @@ beforeEach(() => { afterEach(() => { cleanup() - vi.useRealTimers() vi.clearAllMocks() // Shared singleton client — drop cached skills/toolsets so each test refetches. queryClient.clear() @@ -146,113 +122,6 @@ afterEach(() => { // (2× in a row on PR #93612, plus a main run the same hour). Give this file // headroom; the tests are not slow individually. describe('SkillsView toolset management', { timeout: 60_000 }, () => { - it.each([ - ['collective', 'Collective workspace', 'Plugins workspace'], - ['plugins', 'Plugins workspace', 'Collective workspace'] - ])('opens the %s deep link without replacing the other workspace', async (tab, selected, other) => { - await renderSkills(tab) - - expect(await screen.findByRole('region', { name: selected })).toBeTruthy() - expect(screen.queryByRole('region', { name: other })).toBeNull() - expect(navigateSpy).not.toHaveBeenCalledWith({ pathname: '/skills', search: '', hash: '' }, { replace: true }) - const otherTab = other.replace(' workspace', '') - fireEvent.click(screen.getByRole('button', { name: otherTab })) - expect(navigateSpy).toHaveBeenCalledWith( - { pathname: '/skills', search: `?tab=${otherTab.toLowerCase()}`, hash: '' }, - { replace: true } - ) - }) - - it('hides Collective Wisdom and redirects its deep link when local entitlement is absent', async () => { - getWisdomEntitlement.mockResolvedValue({ entitled: false, org_id: null, scopes: [], expires_at: null }) - await renderSkills('collective') - - expect(screen.queryByRole('button', { name: 'Collective' })).toBeNull() - expect(screen.queryByRole('region', { name: 'Collective workspace' })).toBeNull() - await waitFor(() => - expect(navigateSpy).toHaveBeenCalledWith({ pathname: '/skills', search: '', hash: '' }, { replace: true }) - ) - }) - - it('fails closed when an entitlement response is already expired', async () => { - getWisdomEntitlement.mockResolvedValue({ - entitled: true, - org_id: 'org-1', - scopes: ['wisdom:read'], - expires_at: Date.now() / 1000 - 1 - }) - - await renderSkills('collective') - - expect(screen.queryByRole('button', { name: 'Collective' })).toBeNull() - expect(screen.queryByRole('region', { name: 'Collective workspace' })).toBeNull() - }) - - it('rechecks entitlement after switching away and back while probes are pending', async () => { - const { SkillsView } = await import('./index') - - const page = (profile: string) => ( - - - - - - ) - - const result = render(page('eligible')) - expect(await screen.findByRole('button', { name: 'Collective' })).toBeTruthy() - - getWisdomEntitlement.mockImplementation(() => new Promise(() => {})) - result.rerender(page('other')) - expect(screen.queryByRole('button', { name: 'Collective' })).toBeNull() - result.rerender(page('eligible')) - expect(screen.queryByRole('button', { name: 'Collective' })).toBeNull() - }) - - it('hides Collective when its positive JWT reaches expires_at', async () => { - vi.useFakeTimers() - const expiresAt = Date.now() / 1000 + 1 - getWisdomEntitlement.mockResolvedValue({ - entitled: true, - org_id: 'org-1', - scopes: ['wisdom:read'], - expires_at: expiresAt - }) - - await renderSkills('collective') - expect(screen.queryByRole('region', { name: 'Collective workspace' })).not.toBeNull() - - await act(async () => { - await vi.advanceTimersByTimeAsync(1_001) - }) - - expect(screen.queryByRole('button', { name: 'Collective' })).toBeNull() - expect(screen.queryByRole('region', { name: 'Collective workspace' })).toBeNull() - }) - - it('fails closed when a recheck errors after a positive result', async () => { - vi.useFakeTimers() - getWisdomEntitlement - .mockResolvedValueOnce({ - entitled: true, - org_id: 'org-1', - scopes: ['wisdom:read'], - expires_at: Date.now() / 1000 + 60 - }) - .mockRejectedValue(new Error('probe failed')) - - await renderSkills('collective') - expect(screen.queryByRole('region', { name: 'Collective workspace' })).not.toBeNull() - - await act(async () => { - await vi.advanceTimersByTimeAsync(15_000) - }) - - expect(getWisdomEntitlement).toHaveBeenCalledTimes(2) - expect(screen.queryByRole('button', { name: 'Collective' })).toBeNull() - expect(screen.queryByRole('region', { name: 'Collective workspace' })).toBeNull() - }) - it('renders a switch for each toolset and toggles it off', async () => { await renderSkills() @@ -414,37 +283,6 @@ describe('SkillsView toolset management', { timeout: 60_000 }, () => { expect(await screen.findByText(/Deep research steps/)).toBeTruthy() }) - it('uses editorial skill copy while keeping canonical identifiers for actions', async () => { - getSkills.mockResolvedValue([ - { - name: 'web-research', - description: 'Use when researching the web.', - editorial_name: 'Research the Web', - editorial_description: 'Find and compare trustworthy sources online.', - category: 'research', - enabled: true, - usage: 3, - provenance: 'bundled' - } - ]) - - const { SkillsView } = await import('./index') - await act(async () => { - render( - - - - - - ) - }) - - expect((await screen.findAllByText('Research the Web')).length).toBeGreaterThan(0) - expect(screen.getByText('Find and compare trustworthy sources online.')).toBeTruthy() - expect(screen.queryByText('Use when researching the web.')).toBeNull() - expect(getSkillContent).toHaveBeenCalledWith('web-research', 'default') - }) - it('hub picker refuses to reinstall an already-installed skill', async () => { const { notify } = await import('@/store/notifications') const { EmbeddedHubPicker } = await import('./embedded-hub-picker') diff --git a/apps/desktop/src/app/skills/index.tsx b/apps/desktop/src/app/skills/index.tsx index 94ea38c93c..3a17f97b62 100644 --- a/apps/desktop/src/app/skills/index.tsx +++ b/apps/desktop/src/app/skills/index.tsx @@ -21,7 +21,6 @@ import { getSkills, getToolsets, getUsageAnalytics, - getWisdomEntitlement, previewSkillHub, type ProfileScope, profileScopeKey, @@ -68,7 +67,6 @@ import { TerminalBackendPanel } from '../settings/terminal-backend-panel' import { ToolsetConfigPanel } from '../settings/toolset-config-panel' import type { SetStatusbarItemGroup } from '../shell/statusbar-controls' -import { CollectiveTab } from './collective-tab' import { EmbeddedHubPicker } from './embedded-hub-picker' import { McpTab } from './mcp-tab' import { PluginsTab } from './plugins-tab' @@ -77,7 +75,7 @@ import { $skillsSortDesc, $toolsetsSortDesc } from './store' // 'hub' is gone as a top-level tab — the Skills Hub browser lives inside the // Skills tab now (EmbeddedHubPicker below the installed list). Legacy // `?tab=hub` links fall back to 'skills' via useRouteEnumParam. -const SKILLS_MODES = ['skills', 'toolsets', 'mcp', 'plugins', 'collective'] as const +const SKILLS_MODES = ['skills', 'toolsets', 'mcp', 'plugins'] as const // Skills + toolsets live in the RQ cache so switching tabs/pages paints the // cached lists instantly (no reload flash) and mount only fires a deduped @@ -121,18 +119,6 @@ const usageOf = (skill: SkillInfo): number => (typeof skill.usage === 'number' ? const categoryFor = (skill: SkillInfo): string => asText(skill.category) || 'general' -type SkillPresentation = { - description: string - editorial_description?: string - editorial_name?: string - name: string -} - -const skillDisplayName = (skill: SkillPresentation): string => asText(skill.editorial_name) || skill.name - -const skillDisplayDescription = (skill: SkillPresentation): string => - asText(skill.editorial_description) || skill.description - // Row subtitle: category, with non-default origins badged. function skillSubtitle(skill: SkillInfo): React.ReactNode { const category = prettyName(categoryFor(skill)) @@ -162,14 +148,9 @@ function filteredSkills(skills: SkillInfo[], query: string, desc: boolean): Skil return skills .filter( skill => - !q || - includesQuery(skill.name, q) || - includesQuery(skill.description, q) || - includesQuery(skillDisplayName(skill), q) || - includesQuery(skillDisplayDescription(skill), q) || - includesQuery(skill.category, q) + !q || includesQuery(skill.name, q) || includesQuery(skill.description, q) || includesQuery(skill.category, q) ) - .sort((a, b) => sign * (usageOf(b) - usageOf(a)) || skillDisplayName(a).localeCompare(skillDisplayName(b))) + .sort((a, b) => sign * (usageOf(b) - usageOf(a)) || asText(a.name).localeCompare(asText(b.name))) } // Catalog rows have no usage yet — plain A–Z, same query fields as installed @@ -183,12 +164,10 @@ function filteredOfficial(skills: OfficialSkillInfo[], query: string): OfficialS !q || includesQuery(skill.name, q) || includesQuery(skill.description, q) || - includesQuery(skillDisplayName(skill), q) || - includesQuery(skillDisplayDescription(skill), q) || includesQuery(skill.category, q) || skill.tags.some(tag => includesQuery(tag, q)) ) - .sort((a, b) => skillDisplayName(a).localeCompare(skillDisplayName(b))) + .sort((a, b) => asText(a.name).localeCompare(asText(b.name))) } const toolsetCalls = (toolset: ToolsetInfo, toolCalls: Record): number => @@ -309,71 +288,6 @@ export function SkillsView({ // the wrong machine — withhold it for cross-backend scopes. const crossBackendScope = scopeConnectionId !== null && scopeConnectionId !== (activeGatewayConnectionId() ?? 'local') - // Poll the refresh-free local claim probe so an open page cannot retain a - // positive gate indefinitely. A profile change is fail-closed even if React - // Query has cached that profile from an earlier visit. - const wisdomEntitlement = useQuery({ - queryKey: ['wisdom-entitlement', scopeKey], - queryFn: () => getWisdomEntitlement(scopeProfile), - staleTime: 0, - refetchInterval: 15_000, - retry: false - }) - - // Identity changes on every scope visit, including A -> B -> A. - const wisdomScope = useMemo(() => ({ key: scopeKey }), [scopeKey]) - - const [acceptedWisdomResult, setAcceptedWisdomResult] = useState(null) - - const [entitlementClock, setEntitlementClock] = useState(() => Date.now()) - - useEffect(() => { - if (wisdomEntitlement.isSuccess && wisdomEntitlement.fetchStatus === 'idle') { - setAcceptedWisdomResult({ scope: wisdomScope, updatedAt: wisdomEntitlement.dataUpdatedAt }) - } - }, [wisdomScope, wisdomEntitlement.dataUpdatedAt, wisdomEntitlement.fetchStatus, wisdomEntitlement.isSuccess]) - - const entitlementExpiresAt = wisdomEntitlement.data?.expires_at - useEffect(() => { - if (typeof entitlementExpiresAt !== 'number') { - return - } - - const remaining = entitlementExpiresAt * 1000 - Date.now() - - if (remaining <= 0) { - setEntitlementClock(Date.now()) - - return - } - - const timeout = window.setTimeout(() => setEntitlementClock(Date.now()), Math.min(remaining, 2_147_483_647)) - - return () => window.clearTimeout(timeout) - }, [entitlementExpiresAt]) - - const freshWisdomEntitlement = - wisdomEntitlement.data?.entitled === true && - typeof entitlementExpiresAt === 'number' && - entitlementExpiresAt * 1000 > entitlementClock - - const wisdomEntitled = - acceptedWisdomResult?.scope === wisdomScope && - acceptedWisdomResult.updatedAt === wisdomEntitlement.dataUpdatedAt && - !wisdomEntitlement.isError && - freshWisdomEntitlement - - const wisdomDenied = wisdomEntitlement.isError || (wisdomEntitlement.isSuccess && !freshWisdomEntitlement) - - useEffect(() => { - if (mode === 'collective' && wisdomDenied && !wisdomEntitlement.isFetching) { - setMode('skills') - } - }, [mode, setMode, wisdomDenied, wisdomEntitlement.isFetching]) - const { data: profilesData } = useQuery({ queryKey: ['capabilities-profiles'], queryFn: getProfiles, @@ -950,20 +864,13 @@ export function SkillsView({ // searching it is noise. searchHidden={mode === 'mcp' || mode === 'plugins'} searchHints={searchHints} - searchPlaceholder={ - mode === 'skills' - ? t.skills.searchSkills - : mode === 'collective' && wisdomEntitled - ? t.skills.searchCollective - : t.skills.searchToolsets - } + searchPlaceholder={mode === 'skills' ? t.skills.searchSkills : t.skills.searchToolsets} searchValue={query} tabs={[ { id: 'skills', label: t.skills.tabSkills, meta: skills?.length ?? null }, { id: 'toolsets', label: t.skills.tabToolsets, meta: toolsets ? visibleToolsetCount(toolsets) : null }, { id: 'mcp', label: t.skills.tabMcp }, - { id: 'plugins', label: t.skills.tabPlugins }, - ...(wisdomEntitled ? [{ id: 'collective', label: t.skills.tabCollective }] : []) + { id: 'plugins', label: t.skills.tabPlugins } ]} > {/* One shared column: the scope selector sits above whichever tab is @@ -975,10 +882,8 @@ export function SkillsView({ must not sit under a "Configuring: " header. */} {mode !== 'plugins' && profileScopeSelector}
-
- {mode === 'collective' && wisdomEntitled ? ( - - ) : mode === 'plugins' ? ( +
+ {mode === 'plugins' ? ( // Agent plugins for the scoped profile (selector in the section // header), app-level desktop plugins, the live catalog picker // underneath. Keyed on scope so a profile/connection switch @@ -1053,8 +958,8 @@ export function SkillsView({ }} onToggle={enabled => void handleToggleSkill(skill, enabled)} subtitle={skillSubtitle(skill)} - title={skillDisplayName(skill)} - toggleLabel={skillDisplayName(skill)} + title={skill.name} + toggleLabel={skill.name} /> ))} {/* The built-in optional-skills catalog, below the @@ -1087,7 +992,7 @@ export function SkillsView({ key={skill.identifier} onSelect={() => setSelectedOfficial(skill.identifier)} subtitle={prettyName(skill.category)} - title={skillDisplayName(skill)} + title={skill.name} /> ) })} @@ -1309,7 +1214,7 @@ function SkillDetail({ return ( <> {prettyName(categoryFor(skill))} @@ -1320,7 +1225,7 @@ function SkillDetail({ )} } - title={skillDisplayName(skill)} + title={skill.name} /> {editable && (
@@ -1387,14 +1292,14 @@ function OfficialSkillDetail({ return ( <> {prettyName(skill.category)} {t.skills.officialPill} } - title={skillDisplayName(skill)} + title={skill.name} />
- -
- )} - {supportsPreview && mode === 'preview' ? ( -
- -
- ) : ( -