refactor: move skills state to session context with refreshSkills/removeSkill

Move discoveredSkills signal from AgentBehaviourTab into the session
context so any component can access session.skills(), session.refreshSkills(),
and session.removeSkill(location).

removeSkill optimistically removes the skill from the UI signal before
sending the message to the extension, so the screen updates immediately.
This commit is contained in:
Mark IJbema
2026-03-16 13:04:07 +01:00
parent 2e4070d438
commit 3da18dee40
2 changed files with 36 additions and 24 deletions
@@ -1,4 +1,4 @@
import { Component, createSignal, createMemo, createEffect, For, Show, onCleanup } from "solid-js"
import { Component, createSignal, createMemo, createEffect, For, Show } from "solid-js"
import { Select } from "@kilocode/kilo-ui/select"
import { TextField } from "@kilocode/kilo-ui/text-field"
import { Card } from "@kilocode/kilo-ui/card"
@@ -10,8 +10,7 @@ import { useDialog } from "@kilocode/kilo-ui/context/dialog"
import { useConfig } from "../../context/config"
import { useSession } from "../../context/session"
import { useLanguage } from "../../context/language"
import { useVSCode } from "../../context/vscode"
import type { AgentConfig, SkillInfo, ExtensionMessage } from "../../types/messages"
import type { AgentConfig, SkillInfo } from "../../types/messages"
type SubtabId = "agents" | "mcpServers" | "rules" | "workflows" | "skills"
@@ -54,31 +53,20 @@ const AgentBehaviourTab: Component = () => {
const language = useLanguage()
const { config, updateConfig } = useConfig()
const session = useSession()
const vscode = useVSCode()
const dialog = useDialog()
const [activeSubtab, setActiveSubtab] = createSignal<SubtabId>("agents")
const [selectedAgent, setSelectedAgent] = createSignal<string>("")
const [newSkillPath, setNewSkillPath] = createSignal("")
const [newSkillUrl, setNewSkillUrl] = createSignal("")
const [newInstruction, setNewInstruction] = createSignal("")
const [discoveredSkills, setDiscoveredSkills] = createSignal<SkillInfo[]>([])
// Subscribe to skillsLoaded messages from the extension
const unsub = vscode.onMessage((message: ExtensionMessage) => {
if (message.type === "skillsLoaded") {
setDiscoveredSkills(message.skills)
}
})
// Fetch skills whenever the skills subtab becomes active
createEffect(() => {
if (activeSubtab() === "skills") {
vscode.postMessage({ type: "requestSkills" })
session.refreshSkills()
}
})
onCleanup(() => unsub())
const agentNames = createMemo(() => {
const names = session.agents().map((a) => a.name)
// Also include any agents from config that might not be in the agent list
@@ -182,10 +170,6 @@ const AgentBehaviourTab: Component = () => {
updateConfig({ skills: { ...config().skills, urls: current } })
}
const removeSkill = (skill: SkillInfo) => {
vscode.postMessage({ type: "removeSkill", location: skill.location })
}
const confirmRemoveSkill = (skill: SkillInfo) => {
dialog.show(() => (
<Dialog title={language.t("settings.agentBehaviour.removeSkill.title")} fit>
@@ -199,7 +183,7 @@ const AgentBehaviourTab: Component = () => {
variant="primary"
size="large"
onClick={() => {
removeSkill(skill)
session.removeSkill(skill.location)
dialog.close()
}}
>
@@ -430,7 +414,7 @@ const AgentBehaviourTab: Component = () => {
{language.t("settings.agentBehaviour.discoveredSkills")}
</h4>
<Show
when={discoveredSkills().length > 0}
when={session.skills().length > 0}
fallback={
<Card style={{ "margin-bottom": "16px" }}>
<div data-slot="settings-row-label-subtitle">{language.t("settings.agentBehaviour.noSkillsFound")}</div>
@@ -438,7 +422,7 @@ const AgentBehaviourTab: Component = () => {
}
>
<Card style={{ "margin-bottom": "16px" }}>
<For each={discoveredSkills()}>
<For each={session.skills()}>
{(skill, index) => (
<div
style={{
@@ -446,8 +430,7 @@ const AgentBehaviourTab: Component = () => {
"align-items": "center",
"justify-content": "space-between",
padding: "8px 0",
"border-bottom":
index() < discoveredSkills().length - 1 ? "1px solid var(--border-weak-base)" : "none",
"border-bottom": index() < session.skills().length - 1 ? "1px solid var(--border-weak-base)" : "none",
}}
>
<div style={{ flex: 1, "min-width": 0 }}>
@@ -36,6 +36,7 @@ import type {
ModelSelection,
ContextUsage,
AgentInfo,
SkillInfo,
ExtensionMessage,
FileAttachment,
SendMessageFailedMessage,
@@ -114,6 +115,11 @@ interface SessionContextValue {
totalCost: Accessor<number>
contextUsage: Accessor<ContextUsage | undefined>
// Skills loaded from the CLI backend
skills: Accessor<SkillInfo[]>
refreshSkills: () => void
removeSkill: (location: string) => void
// Agent/mode selection (per-session)
agents: Accessor<AgentInfo[]>
selectedAgent: Accessor<string>
@@ -204,6 +210,9 @@ export const SessionProvider: ParentComponent = (props) => {
const [agents, setAgents] = createSignal<AgentInfo[]>([])
const [defaultAgent, setDefaultAgent] = createSignal("code")
// Skills loaded from the CLI backend
const [skills, setSkills] = createSignal<SkillInfo[]>([])
// Pending agent selection for before a session exists
const [pendingAgentSelection, setPendingAgentSelection] = createSignal<string | null>(null)
@@ -341,8 +350,25 @@ export const SessionProvider: ParentComponent = (props) => {
vscode.postMessage({ type: "requestAgents" })
}, agentRetryMs)
// Skills loaded from the CLI backend
const unsubSkills = vscode.onMessage((message: ExtensionMessage) => {
if (message.type === "skillsLoaded") {
setSkills(message.skills)
}
})
const refreshSkills = () => {
vscode.postMessage({ type: "requestSkills" })
}
const removeSkill = (location: string) => {
setSkills((prev) => prev.filter((s) => s.location !== location))
vscode.postMessage({ type: "removeSkill", location })
}
onCleanup(() => {
unsubAgents()
unsubSkills()
clearInterval(agentRetryTimer)
})
@@ -1285,6 +1311,9 @@ export const SessionProvider: ParentComponent = (props) => {
totalCost,
contextUsage,
agents,
skills,
refreshSkills,
removeSkill,
selectedAgent: selectedAgentName,
selectAgent,
getSessionAgent: (sessionID: string) => store.agentSelections[sessionID] ?? defaultAgent(),