diff --git a/src/components/extensions/new-skills-dialog.tsx b/src/components/extensions/new-skills-dialog.tsx index 7b17202..9fc9de8 100644 --- a/src/components/extensions/new-skills-dialog.tsx +++ b/src/components/extensions/new-skills-dialog.tsx @@ -2,7 +2,6 @@ import { Download, Loader2, Package } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { ScopeTargetField } from "@/components/shared/scope-target-field"; import { useFocusTrap } from "@/hooks/use-focus-trap"; -import { useScope } from "@/hooks/use-scope"; import type { ConfigScope, NewRepoSkill } from "@/lib/types"; import { agentDisplayName, sortAgents } from "@/lib/types"; import { useAgentStore } from "@/stores/agent-store"; @@ -32,13 +31,10 @@ export function NewSkillsDialog({ ); const [selectedAgents, setSelectedAgents] = useState>(new Set()); const [installing, setInstalling] = useState(false); - const { scope, isAll } = useScope(); - // Single-scope mode: the active scope is the install target. All-scopes - // mode: user must pick via ScopeTargetField (null until picked). + // The dialog appears unexpectedly after Check Updates discovery, so the + // active UI scope is not necessarily where the user wants these skills. + // Always require an explicit pick (no implicit "use current scope"). const [pickedScope, setPickedScope] = useState(null); - const effectiveTarget: ConfigScope | null = isAll - ? pickedScope - : (scope as ConfigScope); const agents = useAgentStore((s) => s.agents); const agentOrder = useAgentStore((s) => s.agentOrder); @@ -105,8 +101,19 @@ export function NewSkillsDialog({ } }; + const allSkillsSelected = + skills.length > 0 && selected.size === skills.length; + + const toggleAllSkills = () => { + if (allSkillsSelected) { + setSelected(new Set()); + } else { + setSelected(new Set(skills.map((s) => `${s.repo_url}::${s.skill_id}`))); + } + }; + const handleInstall = async () => { - if (!effectiveTarget) return; + if (!pickedScope) return; setInstalling(true); try { const targetAgents = [...selectedAgents]; @@ -116,7 +123,7 @@ export function NewSkillsDialog({ ); if (selectedSkills.length === 0) continue; const skillIds = selectedSkills.map((s) => s.skill_id); - await onInstall(url, skillIds, targetAgents, effectiveTarget); + await onInstall(url, skillIds, targetAgents, pickedScope); } onClose(); } catch (e: unknown) { @@ -129,7 +136,7 @@ export function NewSkillsDialog({ const selectedCount = selected.size; const canInstall = - selectedCount > 0 && selectedAgents.size > 0 && effectiveTarget !== null; + selectedCount > 0 && selectedAgents.size > 0 && pickedScope !== null; return (
+ {/* Select All — toggle all skills across all repo groups */} + + {/* Skill list grouped by repo */}
{[...grouped].map(([url, group]) => ( @@ -199,9 +217,13 @@ export function NewSkillsDialog({ ))}
- {/* Scope picker (All-scopes mode) / scope hint (single-scope mode) */} + {/* Scope picker — always shown, no implicit "use current scope" */}
- +
{/* Agent selection */} diff --git a/src/components/shared/scope-target-field.tsx b/src/components/shared/scope-target-field.tsx index cb6024f..63dac92 100644 --- a/src/components/shared/scope-target-field.tsx +++ b/src/components/shared/scope-target-field.tsx @@ -13,18 +13,24 @@ interface ScopeTargetFieldProps { onChange: (scope: ConfigScope | null) => void; /** Optional smart default to suggest in All-scopes mode. */ smartDefault?: ConfigScope; + /** When true, always render the picker — even in single-scope mode. + * Used by NewSkillsDialog where the dialog appears unexpectedly + * (post Check Updates discovery) and the active UI scope is not + * necessarily where the user wants the new skills installed. */ + alwaysPick?: boolean; } export function ScopeTargetField({ value, onChange, smartDefault, + alwaysPick = false, }: ScopeTargetFieldProps) { const { scope } = useScope(); const projects = useProjectStore((s) => s.projects); // Single-scope mode: render a static hint, no picker - if (scope.type !== "all") { + if (scope.type !== "all" && !alwaysPick) { return ( diff --git a/src/lib/__tests__/scope-target-field.test.tsx b/src/lib/__tests__/scope-target-field.test.tsx index 0ed3dee..0921f35 100644 --- a/src/lib/__tests__/scope-target-field.test.tsx +++ b/src/lib/__tests__/scope-target-field.test.tsx @@ -75,6 +75,34 @@ describe("ScopeTargetField", () => { expect(onChange).toHaveBeenCalledWith({ type: "global" }); }); + it("renders the picker (not the hint) in single-scope mode when alwaysPick is set", () => { + useScopeStore.setState({ + current: { type: "project", name: "alpha", path: "/p/alpha" }, + hydrated: true, + }); + useProjectStore.setState({ + projects: [ + { + id: "alpha", + name: "alpha", + path: "/p/alpha", + created_at: "", + exists: true, + }, + ], + loading: false, + loaded: true, + }); + render( + wrap( {}} alwaysPick />), + ); + const select = screen.getByLabelText( + /install to scope/i, + ) as HTMLSelectElement; + expect(select).toBeTruthy(); + expect(select.value).toBe(""); + }); + it("shows smart-default 'Use X' shortcut when value is null and smartDefault provided", () => { useScopeStore.setState({ current: { type: "all" }, hydrated: true }); useProjectStore.setState({