mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-29 03:44:06 +08:00
Merge pull request #12416 from Kilo-Org/vigorous-operation
feat(jetbrains): add skills settings
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": minor
|
||||
---
|
||||
|
||||
Support viewing, opening, editing, deleting, and configuring JetBrains skill sources.
|
||||
+8
@@ -820,11 +820,13 @@ class KiloBackendAppService private constructor(
|
||||
}
|
||||
}
|
||||
"global.disposed" -> {
|
||||
logSessionDisposalRisk("global.disposed")
|
||||
log.info("SSE global.disposed — triggering full application reload")
|
||||
val current = _appState.value
|
||||
if (current is KiloAppState.Ready) load()
|
||||
}
|
||||
"server.instance.disposed" -> {
|
||||
logSessionDisposalRisk("server.instance.disposed")
|
||||
log.info("SSE server.instance.disposed — triggering full application reload")
|
||||
val current = _appState.value
|
||||
if (current is KiloAppState.Ready) load()
|
||||
@@ -835,6 +837,12 @@ class KiloBackendAppService private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun logSessionDisposalRisk(event: String) {
|
||||
val active = sessions.statuses.value.filterValues { it.type != "idle" }
|
||||
if (active.isEmpty()) return
|
||||
log.warn("SSE $event while sessions are active; sessions may be cancelled count=${active.size} statuses=${active.values.map { it.type }.distinct()}")
|
||||
}
|
||||
|
||||
private suspend fun clear() {
|
||||
synchronized(loadLock) {
|
||||
val jobs = listOfNotNull(loader, eventWatcher)
|
||||
|
||||
+4
@@ -86,6 +86,10 @@ class KiloBackendSessionManager(
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
val active = _statuses.value.filterValues { it.type != "idle" }
|
||||
if (active.isNotEmpty()) {
|
||||
log.warn("Session manager stopping with active sessions count=${active.size} statuses=${active.values.map { it.type }.distinct()}")
|
||||
}
|
||||
watcher?.cancel()
|
||||
watcher = null
|
||||
client = null
|
||||
|
||||
+6
-1
@@ -647,7 +647,12 @@ object KiloCliDataParser {
|
||||
val obj = item.obj() ?: return@mapNotNull null
|
||||
val name = obj.str("name") ?: return@mapNotNull null
|
||||
val location = obj.str("location") ?: return@mapNotNull null
|
||||
SkillDto(name = name, description = obj.str("description"), location = location)
|
||||
SkillDto(
|
||||
name = name,
|
||||
description = obj.str("description"),
|
||||
location = location,
|
||||
content = obj.str("content"),
|
||||
)
|
||||
}
|
||||
|
||||
fun parseAgentBehaviorCommands(raw: String): List<CommandDto> =
|
||||
|
||||
+159
-1
@@ -14,6 +14,7 @@ import ai.kilocode.rpc.dto.ConfigPatchDto
|
||||
import ai.kilocode.rpc.dto.McpConfigDto
|
||||
import ai.kilocode.rpc.dto.McpServerConfigDto
|
||||
import ai.kilocode.rpc.dto.PermissionRuleItemDto
|
||||
import ai.kilocode.rpc.dto.SkillDto
|
||||
import com.intellij.openapi.components.service
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -22,7 +23,11 @@ import kotlinx.serialization.json.JsonPrimitive
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import com.intellij.openapi.util.SystemInfo
|
||||
import java.net.URLEncoder
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.InvalidPathException
|
||||
import java.nio.file.Path
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
@@ -33,6 +38,7 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? =
|
||||
private val JSON = "application/json".toMediaType()
|
||||
private val saved = ConcurrentHashMap<String, SavedMcp>()
|
||||
private val port = AtomicInteger(-1)
|
||||
private val extensions = setOf("md", "markdown", "txt", "text", "html", "htm")
|
||||
}
|
||||
|
||||
private val app: KiloBackendAppService get() = backend ?: service()
|
||||
@@ -56,11 +62,59 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? =
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun skills(directory: String) = KiloCliDataParser.parseAgentBehaviorSkills(request(directory, "/skill", null))
|
||||
override suspend fun skills(directory: String): List<SkillDto> {
|
||||
val items = KiloCliDataParser.parseAgentBehaviorSkills(request(directory, "/skill", null))
|
||||
return items.map { item ->
|
||||
val editable = editable(item)
|
||||
item.copy(content = skillContent(item) ?: item.content, editable = editable)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun removeSkill(directory: String, location: String): Boolean =
|
||||
post(directory, "/kilocode/skill/remove", JsonObject(mapOf("location" to JsonPrimitive(location))))
|
||||
|
||||
override suspend fun reloadSkills(directory: String): Boolean {
|
||||
LOG.info("Skills reload requested dir=$directory")
|
||||
if (hasActiveSession(directory)) {
|
||||
LOG.warn("Skills reload blocked by active session dir=$directory")
|
||||
return false
|
||||
}
|
||||
runCatching { post(directory, "/instance/reload") }.onFailure { err ->
|
||||
LOG.warn("Skills reload failed dir=$directory", err)
|
||||
}.getOrThrow()
|
||||
LOG.info("Skills reload succeeded dir=$directory")
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun saveSkill(directory: String, location: String, content: String): Boolean {
|
||||
LOG.info("Skill save requested dir=$directory location=$location")
|
||||
app.requireReady()
|
||||
val paths = knownSkills(directory)
|
||||
val path = writablePath(directory, location, paths) ?: return false
|
||||
withContext(Dispatchers.IO) {
|
||||
Files.writeString(path, content, StandardCharsets.UTF_8)
|
||||
}
|
||||
LOG.info("Skill file saved dir=$directory path=$path bytes=${content.toByteArray(StandardCharsets.UTF_8).size}")
|
||||
LOG.info("Skill save reload deferred dir=$directory path=$path")
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun saveSkills(directory: String, edits: Map<String, String>): Boolean {
|
||||
LOG.info("Skills save requested dir=$directory count=${edits.size}")
|
||||
app.requireReady()
|
||||
val known = knownSkills(directory)
|
||||
val paths = edits.mapNotNull { (location, content) ->
|
||||
val path = writablePath(directory, location, known) ?: return false
|
||||
path to content
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
for ((path, content) in paths) Files.writeString(path, content, StandardCharsets.UTF_8)
|
||||
}
|
||||
LOG.info("Skill files saved dir=$directory count=${paths.size}")
|
||||
LOG.info("Skills save reload deferred dir=$directory count=${paths.size}")
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun removeAgent(directory: String, name: String): Boolean =
|
||||
post(directory, "/kilocode/agent/remove", JsonObject(mapOf("name" to JsonPrimitive(name))))
|
||||
|
||||
@@ -139,6 +193,104 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? =
|
||||
return true
|
||||
}
|
||||
|
||||
private fun hasActiveSession(directory: String): Boolean {
|
||||
val active = app.sessions.statuses.value.filterValues { it.type != "idle" }
|
||||
if (active.isNotEmpty()) {
|
||||
LOG.info("Skills reload active statuses dir=$directory count=${active.size} types=${active.values.map { it.type }.distinct()}")
|
||||
return true
|
||||
}
|
||||
val permissions = runCatching { app.chat.pendingPermissions(directory) }.onFailure { err ->
|
||||
LOG.warn("Skills reload pending permission check failed dir=$directory", err)
|
||||
}.getOrDefault(emptyList())
|
||||
if (permissions.isNotEmpty()) {
|
||||
LOG.info("Skills reload pending permissions dir=$directory count=${permissions.size}")
|
||||
return true
|
||||
}
|
||||
val questions = runCatching { app.chat.pendingQuestions(directory) }.onFailure { err ->
|
||||
LOG.warn("Skills reload pending question check failed dir=$directory", err)
|
||||
}.getOrDefault(emptyList())
|
||||
if (questions.isNotEmpty()) {
|
||||
LOG.info("Skills reload pending questions dir=$directory count=${questions.size}")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private suspend fun skillContent(skill: SkillDto): String? {
|
||||
val path = resolveSkillPath(skill.location) ?: return null
|
||||
return runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (!Files.isRegularFile(path)) null else Files.readString(path, StandardCharsets.UTF_8)
|
||||
}
|
||||
}.onFailure { err ->
|
||||
LOG.warn("Skill content read failed: $path", err)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun editable(skill: SkillDto): Boolean {
|
||||
val path = resolveSkillPath(skill.location) ?: return false
|
||||
if (urlCached(path)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
private suspend fun knownSkills(directory: String): Set<Path> {
|
||||
val items = KiloCliDataParser.parseAgentBehaviorSkills(request(directory, "/skill", null))
|
||||
return items.mapNotNull { item -> resolveEditablePath(item) }.toSet()
|
||||
}
|
||||
|
||||
private fun writablePath(directory: String, location: String, known: Set<Path>): Path? {
|
||||
val path = resolveSkillPath(location)
|
||||
if (path == null) {
|
||||
LOG.warn("Skill save rejected: invalid location dir=$directory location=$location")
|
||||
return null
|
||||
}
|
||||
if (path !in known) {
|
||||
LOG.warn("Skill save rejected: unknown skill dir=$directory path=$path")
|
||||
return null
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
private fun resolveEditablePath(skill: SkillDto): Path? {
|
||||
val path = resolveSkillPath(skill.location) ?: return null
|
||||
if (urlCached(path)) return null
|
||||
return path
|
||||
}
|
||||
|
||||
private fun resolveSkillPath(location: String): Path? {
|
||||
val raw = normalizeWorkspacePath(location) ?: return null
|
||||
val path = try {
|
||||
Path.of(raw).normalize()
|
||||
} catch (_: InvalidPathException) {
|
||||
return null
|
||||
}
|
||||
if (!path.isAbsolute || !isSkillFile(path)) return null
|
||||
return path
|
||||
}
|
||||
|
||||
private fun urlCached(path: Path): Boolean {
|
||||
return cacheRoots().any { root -> path.startsWith(root.resolve("kilo").resolve("skills").normalize()) }
|
||||
}
|
||||
|
||||
private fun cacheRoots(): Set<Path> = buildSet {
|
||||
val home = System.getProperty("user.home")
|
||||
add(Path.of(cacheRoot()).normalize())
|
||||
add(Path.of(home, ".cache").normalize())
|
||||
add(Path.of(home, "Library", "Caches").normalize())
|
||||
System.getenv("LOCALAPPDATA")?.takeIf { it.isNotBlank() }?.let { add(Path.of(it).normalize()) }
|
||||
add(Path.of(home, "AppData", "Local").normalize())
|
||||
}
|
||||
|
||||
private fun cacheRoot(): String {
|
||||
val xdg = System.getenv("XDG_CACHE_HOME")?.takeIf { it.isNotBlank() }
|
||||
if (xdg != null) return xdg
|
||||
val home = System.getProperty("user.home")
|
||||
if (SystemInfo.isMac) return Path.of(home, "Library", "Caches").toString()
|
||||
if (SystemInfo.isWindows) return System.getenv("LOCALAPPDATA")?.takeIf { it.isNotBlank() }
|
||||
?: Path.of(home, "AppData", "Local").toString()
|
||||
return Path.of(home, ".cache").toString()
|
||||
}
|
||||
|
||||
private suspend fun patchConfig(path: String, body: String): Unit = withContext(Dispatchers.IO) {
|
||||
val http = app.http ?: throw IllegalStateException("Kilo HTTP client is unavailable")
|
||||
val url = "http://127.0.0.1:${app.port}$path"
|
||||
@@ -234,6 +386,12 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? =
|
||||
|
||||
private fun encodePath(value: String): String = encode(value).replace("+", "%20")
|
||||
|
||||
private fun isSkillFile(path: Path): Boolean {
|
||||
val name = path.fileName?.toString() ?: return false
|
||||
if (name == "SKILL.md") return true
|
||||
return name.substringAfterLast('.', "").lowercase() in extensions
|
||||
}
|
||||
|
||||
private data class SavedMcp(
|
||||
val directory: String,
|
||||
val name: String,
|
||||
|
||||
+2
@@ -64,6 +64,8 @@ internal object KiloWorkspaceDtoMapper {
|
||||
name = s.name,
|
||||
description = s.description,
|
||||
location = s.location,
|
||||
content = s.content,
|
||||
editable = false,
|
||||
)
|
||||
|
||||
private fun provider(p: ProviderInfo) = ProviderDto(
|
||||
|
||||
+1
-1
@@ -341,7 +341,7 @@ class KiloWorkspaceRpcApiImpl internal constructor(
|
||||
}
|
||||
descriptor.navigate(true)
|
||||
if (cont.isActive) cont.resume(Unit)
|
||||
}, ModalityState.any())
|
||||
}, ModalityState.nonModal())
|
||||
}
|
||||
|
||||
private fun project(path: Path): Project? {
|
||||
|
||||
+1
@@ -254,6 +254,7 @@ class KiloBackendWorkspace(
|
||||
name = s.name,
|
||||
description = s.description,
|
||||
location = s.location,
|
||||
content = s.content,
|
||||
)
|
||||
})
|
||||
} catch (e: CancellationException) {
|
||||
|
||||
+1
@@ -143,4 +143,5 @@ data class SkillInfo(
|
||||
val name: String,
|
||||
val description: String?,
|
||||
val location: String,
|
||||
val content: String?,
|
||||
)
|
||||
|
||||
+11
-3
@@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import okhttp3.Request
|
||||
import okhttp3.sse.EventSource
|
||||
import okhttp3.sse.EventSourceListener
|
||||
@@ -36,6 +37,10 @@ import kotlin.test.assertTrue
|
||||
|
||||
class KiloConnectionServiceTest {
|
||||
|
||||
private companion object {
|
||||
const val WAIT_MS = 15_000L
|
||||
}
|
||||
|
||||
private val mock = MockCliServer()
|
||||
private val fake = FakeCliServer(mock)
|
||||
private val log = TestLog()
|
||||
@@ -101,20 +106,23 @@ class KiloConnectionServiceTest {
|
||||
val svc = KiloConnectionService(scope, server, {}, log)
|
||||
val job = scope.launch { svc.connect() }
|
||||
|
||||
val downloading = withTimeout(5_000) {
|
||||
val downloading = withTimeout(WAIT_MS) {
|
||||
svc.state.first { it is ConnectionState.Downloading }
|
||||
}
|
||||
assertEquals(ConnectionState.Downloading(42, "1.2.3", "darwin-arm64"), downloading)
|
||||
|
||||
resolved.complete(Unit)
|
||||
withTimeout(5_000) {
|
||||
withTimeout(WAIT_MS) {
|
||||
svc.state.first { it == ConnectionState.Connecting }
|
||||
}
|
||||
|
||||
ready.complete(Unit)
|
||||
withTimeout(5_000) {
|
||||
val connected = withTimeoutOrNull(WAIT_MS) {
|
||||
svc.state.first { it is ConnectionState.Connected }
|
||||
}
|
||||
if (connected == null) {
|
||||
error("Timed out waiting for Connected after CLI ready; state=${svc.state.value}; logs=${log.messages.joinToString("\n")}")
|
||||
}
|
||||
job.join()
|
||||
}
|
||||
|
||||
|
||||
+152
@@ -14,6 +14,8 @@ import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContains
|
||||
@@ -78,6 +80,156 @@ class KiloAgentBehaviorRpcApiImplTest {
|
||||
assertContains(err.message.orEmpty(), "HTTP 400")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `skills and remove skill call CLI endpoints`() = runBlocking {
|
||||
val dir = Files.createTempDirectory("kilo-skill-test")
|
||||
val file = Files.createDirectories(dir.resolve("plan")).resolve("SKILL.md")
|
||||
val content = """---
|
||||
|name: plan
|
||||
|description: Plan work
|
||||
|---
|
||||
|
|
||||
|# Fresh Plan
|
||||
""".trimMargin()
|
||||
Files.writeString(file, content)
|
||||
mock.skills = """[
|
||||
{"name":"plan","description":"Plan work","location":"$file","content":"# Stale Plan"},
|
||||
{"name":"builtin","location":"builtin"}
|
||||
]""".trimIndent()
|
||||
val rpc = rpc()
|
||||
|
||||
val skills = rpc.skills("/test project")
|
||||
assertEquals(listOf("plan", "builtin"), skills.map { it.name })
|
||||
assertEquals("Plan work", skills.single { it.name == "plan" }.description)
|
||||
assertEquals(content, skills.single { it.name == "plan" }.content)
|
||||
assertEquals(true, skills.single { it.name == "plan" }.editable)
|
||||
assertEquals(false, skills.single { it.name == "builtin" }.editable)
|
||||
|
||||
assertTrue(rpc.removeSkill("/test project", file.toString()))
|
||||
assertEquals("{\"location\":\"$file\"}", mock.lastSkillRemoveBody)
|
||||
assertEquals(1, mock.requestCount("/kilocode/skill/remove"))
|
||||
|
||||
mock.skillRemoveStatus = 400
|
||||
val err = assertFailsWith<RuntimeException> {
|
||||
rpc.removeSkill("/test", "/tmp/missing/SKILL.md")
|
||||
}
|
||||
assertContains(err.message.orEmpty(), "HTTP 400")
|
||||
|
||||
assertTrue(rpc.reloadSkills("/test project"))
|
||||
assertEquals(1, mock.requestCount("/instance/reload"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `url cached skills are read only`() = runBlocking {
|
||||
val cache = Path.of(System.getProperty("user.home"), ".cache", "kilo", "skills", "remote")
|
||||
val file = Files.createDirectories(cache).resolve("SKILL.md")
|
||||
Files.writeString(file, "# Remote")
|
||||
mock.skills = """[
|
||||
{"name":"remote","description":"Remote","location":"$file","content":"# Remote"}
|
||||
]""".trimIndent()
|
||||
|
||||
val skill = rpc().skills("/test project").single()
|
||||
|
||||
assertEquals(false, skill.editable)
|
||||
assertEquals("# Remote", skill.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `custom skills under non cache paths remain editable`() = runBlocking {
|
||||
val dir = Files.createTempDirectory("kilo-skill-test")
|
||||
val file = Files.createDirectories(dir.resolve("cache/kilo/skills/custom")).resolve("SKILL.md")
|
||||
Files.writeString(file, "# Custom")
|
||||
mock.skills = """[
|
||||
{"name":"custom","description":"Custom","location":"$file","content":"# Custom"}
|
||||
]""".trimIndent()
|
||||
|
||||
val skill = rpc().skills("/test project").single()
|
||||
|
||||
assertEquals(true, skill.editable)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `save skill supports configured markdown text and html files without reload`() = runBlocking {
|
||||
val dir = Files.createTempDirectory("kilo-skill-test")
|
||||
val file = dir.resolve("test.md")
|
||||
Files.writeString(file, "old")
|
||||
mock.skills = """[
|
||||
{"name":"test","description":"Test","location":"$file","content":"old"}
|
||||
]""".trimIndent()
|
||||
val rpc = rpc()
|
||||
|
||||
assertTrue(rpc.saveSkill("/test project", file.toString(), "new content"))
|
||||
assertEquals("new content", Files.readString(file))
|
||||
assertEquals(0, mock.requestCount("/instance/reload"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `save skill writes content without reloading instance`() = runBlocking {
|
||||
val dir = Files.createTempDirectory("kilo-skill-test")
|
||||
val file = Files.createDirectories(dir.resolve("plan")).resolve("SKILL.md")
|
||||
Files.writeString(file, "old")
|
||||
mock.skills = """[
|
||||
{"name":"plan","description":"Plan work","location":"$file","content":"old"}
|
||||
]""".trimIndent()
|
||||
val rpc = rpc()
|
||||
|
||||
assertTrue(rpc.saveSkill("/test project", file.toString(), "new content"))
|
||||
|
||||
assertEquals("new content", Files.readString(file))
|
||||
assertEquals(0, mock.requestCount("/instance/reload"))
|
||||
assertFalse(rpc.saveSkill("/test project", "builtin", "nope"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `save skills validates known paths once for multiple edits`() = runBlocking {
|
||||
val dir = Files.createTempDirectory("kilo-skill-test")
|
||||
val plan = Files.createDirectories(dir.resolve("plan")).resolve("SKILL.md")
|
||||
val review = Files.createDirectories(dir.resolve("review")).resolve("SKILL.md")
|
||||
Files.writeString(plan, "old plan")
|
||||
Files.writeString(review, "old review")
|
||||
mock.skills = """[
|
||||
{"name":"plan","description":"Plan work","location":"$plan","content":"old plan"},
|
||||
{"name":"review","description":"Review work","location":"$review","content":"old review"}
|
||||
]""".trimIndent()
|
||||
val rpc = rpc()
|
||||
mock.resetCounts()
|
||||
|
||||
assertTrue(rpc.saveSkills("/test project", mapOf(plan.toString() to "new plan", review.toString() to "new review")))
|
||||
|
||||
assertEquals("new plan", Files.readString(plan))
|
||||
assertEquals("new review", Files.readString(review))
|
||||
assertEquals(1, mock.requestCount("/skill"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `save skill rejects unknown absolute skill files`() = runBlocking {
|
||||
val dir = Files.createTempDirectory("kilo-skill-test")
|
||||
val known = Files.createDirectories(dir.resolve("known")).resolve("SKILL.md")
|
||||
val other = Files.createDirectories(dir.resolve("other")).resolve("SKILL.md")
|
||||
Files.writeString(known, "known")
|
||||
Files.writeString(other, "old")
|
||||
mock.skills = """[
|
||||
{"name":"known","description":"Known","location":"$known","content":"known"}
|
||||
]""".trimIndent()
|
||||
|
||||
assertFalse(rpc().saveSkill("/test project", other.toString(), "new content"))
|
||||
|
||||
assertEquals("old", Files.readString(other))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reload skills is blocked by pending permissions`() = runBlocking {
|
||||
mock.pendingPermissions = """[
|
||||
{"id":"per_test","sessionID":"ses_test","permission":"bash","patterns":["*"],"metadata":{}}
|
||||
]""".trimIndent()
|
||||
val rpc = rpc()
|
||||
|
||||
assertFalse(rpc.reloadSkills("/test project"))
|
||||
|
||||
assertEquals(1, mock.requestCount("/permission"))
|
||||
assertEquals(0, mock.requestCount("/instance/reload"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mcp config writes global and workspace patches`() = runBlocking {
|
||||
mock.config = """{"mcp":{"global":{"type":"local","command":["node","g.js"]}}}"""
|
||||
|
||||
+11
@@ -68,9 +68,11 @@ class MockCliServer : AutoCloseable {
|
||||
@Volatile var mcpStatus = 200
|
||||
@Volatile var mcpActionStatus = 200
|
||||
@Volatile var agentRemoveStatus = 200
|
||||
@Volatile var skillRemoveStatus = 200
|
||||
@Volatile var agentBuilderStatus = 200
|
||||
@Volatile var lastMcpActionPath: String? = null
|
||||
@Volatile var lastAgentRemoveBody: String? = null
|
||||
@Volatile var lastSkillRemoveBody: String? = null
|
||||
@Volatile var lastAgentBuilderPath: String? = null
|
||||
@Volatile var lastAgentBuilderBody: String? = null
|
||||
@Volatile var lastAgentBuilderMethod: String? = null
|
||||
@@ -135,6 +137,8 @@ class MockCliServer : AutoCloseable {
|
||||
@Volatile var lastSessionRenamePath: String? = null
|
||||
@Volatile var lastSessionRenameBody: String? = null
|
||||
@Volatile var lastSessionRenameMethod: String? = null
|
||||
@Volatile var pendingPermissions = "[]"
|
||||
@Volatile var pendingQuestions = "[]"
|
||||
|
||||
/** Configurable delay for all endpoint responses (ms). 0 = no delay. */
|
||||
@Volatile var responseDelay: Long = 0
|
||||
@@ -375,6 +379,11 @@ class MockCliServer : AutoCloseable {
|
||||
lastAgentRemoveBody = body
|
||||
respond(output, agentRemoveStatus, if (agentRemoveStatus == 200) "true" else """{"error":"Agent not found"}""")
|
||||
}
|
||||
bare == "/kilocode/skill/remove" && method == "POST" -> {
|
||||
lastSkillRemoveBody = body
|
||||
respond(output, skillRemoveStatus, if (skillRemoveStatus == 200) "true" else """{"error":"Skill not found"}""")
|
||||
}
|
||||
bare == "/instance/reload" && method == "POST" -> respond(output, 200, "true")
|
||||
bare == "/command" -> respond(output, commandsStatus, commands)
|
||||
bare == "/skill" -> respond(output, skillsStatus, skills)
|
||||
bare == "/find/file" -> {
|
||||
@@ -405,6 +414,8 @@ class MockCliServer : AutoCloseable {
|
||||
respond(output, cloudSessionImportStatus, cloudSessionImport)
|
||||
}
|
||||
bare == "/session/status" -> respond(output, sessionStatusesStatus, sessionStatuses)
|
||||
bare == "/permission" && method == "GET" -> respond(output, 200, pendingPermissions)
|
||||
bare == "/question" && method == "GET" -> respond(output, 200, pendingQuestions)
|
||||
bare == "/session" && method == "GET" -> respond(output, sessionsStatus, sessions)
|
||||
bare == "/session" && method == "POST" -> respond(output, sessionCreateStatus, sessionCreate)
|
||||
bare.matches(Regex("/session/ses_[^/]+")) && method == "GET" ->
|
||||
|
||||
+9
@@ -21,4 +21,13 @@ object KiloNotifications {
|
||||
?: Notification(GROUP, title, content ?: "", NotificationType.ERROR)
|
||||
notification.notify(project)
|
||||
}
|
||||
|
||||
fun info(title: String, content: String? = null) {
|
||||
val project = ProjectManager.getInstance().openProjects.firstOrNull { !it.isDefault }
|
||||
val notification = NotificationGroupManager.getInstance()
|
||||
.getNotificationGroup(GROUP)
|
||||
?.createNotification(title, content ?: "", NotificationType.INFORMATION)
|
||||
?: Notification(GROUP, title, content ?: "", NotificationType.INFORMATION)
|
||||
notification.notify(project)
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -33,7 +33,9 @@ class KiloAgentBehaviorService internal constructor(
|
||||
|
||||
suspend fun agents(directory: String): List<AgentDetailDto> = safe(emptyList()) { call { agents(directory) } }
|
||||
|
||||
suspend fun skills(directory: String): List<SkillDto> = safe(emptyList()) { call { skills(directory) } }
|
||||
suspend fun loadSkills(directory: String): List<SkillDto> = call { skills(directory) }
|
||||
|
||||
suspend fun refreshSkills(directory: String, fallback: List<SkillDto>): List<SkillDto> = safe(fallback) { call { skills(directory) } }
|
||||
|
||||
suspend fun commands(directory: String): List<CommandDto> = safe(emptyList()) { call { commands(directory) } }
|
||||
|
||||
@@ -52,6 +54,11 @@ class KiloAgentBehaviorService internal constructor(
|
||||
|
||||
suspend fun removeSkill(directory: String, location: String): Boolean = safe(false) { call { removeSkill(directory, location) } }
|
||||
|
||||
suspend fun reloadSkills(directory: String): Boolean = safe(false) { call { reloadSkills(directory) } }
|
||||
|
||||
suspend fun saveSkills(directory: String, edits: Map<String, String>): Boolean =
|
||||
safe(false) { call { saveSkills(directory, edits) } }
|
||||
|
||||
suspend fun removeAgent(directory: String, name: String): Boolean = safe(false) { call { removeAgent(directory, name) } }
|
||||
|
||||
suspend fun createAgent(directory: String, input: AgentCreateDto): Boolean = safe(false) { call { createAgent(directory, input) } }
|
||||
|
||||
+9
@@ -169,6 +169,15 @@ class KiloWorkspaceService internal constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun openFile(path: String, line: Int? = null, column: Int? = null): Boolean {
|
||||
return try {
|
||||
call { openFile(path, line, column) }
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("workspace file open failed for path=$path", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun localConfigTarget(directory: String): ConfigTargetDto? {
|
||||
return try {
|
||||
val target = call { this.localConfigTarget(directory) }
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ class AgentBehaviorConfigurable : SearchableConfigurable {
|
||||
listOf(
|
||||
KiloBundle.message("settings.agentBehavior.agents.displayName") to AgentsConfigurable.ID,
|
||||
KiloBundle.message("settings.agentBehavior.mcp.displayName") to McpConfigurable.ID,
|
||||
KiloBundle.message("settings.agentBehavior.skills.displayName") to SkillsConfigurable.ID,
|
||||
).forEach { (label, id) ->
|
||||
panel.next(ActionLink(label) { e ->
|
||||
val src = e.source as? JComponent ?: return@ActionLink
|
||||
|
||||
+586
@@ -0,0 +1,586 @@
|
||||
package ai.kilocode.client.settings.agents
|
||||
|
||||
import ai.kilocode.client.app.KiloAgentBehaviorService
|
||||
import ai.kilocode.client.app.KiloAppService
|
||||
import ai.kilocode.client.app.KiloWorkspaceService
|
||||
import ai.kilocode.client.KiloNotifications
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.settings.base.SettingsBadge
|
||||
import ai.kilocode.client.settings.base.SettingsDraftPage
|
||||
import ai.kilocode.client.settings.base.SettingsDraftState
|
||||
import ai.kilocode.client.settings.base.SettingsListCell
|
||||
import ai.kilocode.client.settings.base.SettingsListConfig
|
||||
import ai.kilocode.client.settings.base.SettingsListItem
|
||||
import ai.kilocode.client.settings.base.SettingsListPanel
|
||||
import ai.kilocode.client.settings.base.SettingsListSelection
|
||||
import ai.kilocode.client.settings.base.SettingsListView
|
||||
import ai.kilocode.client.settings.base.SettingsMessageException
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.layout.Stack
|
||||
import ai.kilocode.log.KiloLog
|
||||
import ai.kilocode.rpc.dto.ConfigPatchDto
|
||||
import ai.kilocode.rpc.dto.SkillsConfigDto
|
||||
import ai.kilocode.rpc.dto.SkillsPatchDto
|
||||
import ai.kilocode.rpc.dto.SkillDto
|
||||
import com.intellij.CommonBundle
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.actionSystem.ActionManager
|
||||
import com.intellij.openapi.actionSystem.ActionPlaces
|
||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.DefaultActionGroup
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.application.asContextElement
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.editor.EditorFactory
|
||||
import com.intellij.openapi.editor.event.DocumentEvent
|
||||
import com.intellij.openapi.editor.event.DocumentListener
|
||||
import com.intellij.openapi.fileChooser.FileChooser
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptor
|
||||
import com.intellij.openapi.fileTypes.FileType
|
||||
import com.intellij.openapi.fileTypes.FileTypeManager
|
||||
import com.intellij.openapi.fileTypes.PlainTextFileType
|
||||
import com.intellij.openapi.fileTypes.UnknownFileType
|
||||
import com.intellij.openapi.project.DumbAwareAction
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import com.intellij.openapi.ui.DialogWrapper
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.ui.EditorTextField
|
||||
import com.intellij.ui.TitledSeparator
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.ui.components.JBTextField
|
||||
import com.intellij.util.ui.JBUI
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import java.awt.BorderLayout
|
||||
import javax.swing.JButton
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.ScrollPaneConstants
|
||||
import javax.swing.ListSelectionModel
|
||||
|
||||
private val edt = Dispatchers.EDT + ModalityState.any().asContextElement()
|
||||
|
||||
class SkillsConfigurable : AgentBehaviorConfigurableBase<JComponent>() {
|
||||
override fun getId(): String = ID
|
||||
override fun getDisplayName(): String = KiloBundle.message("settings.agentBehavior.skills.displayName")
|
||||
override fun create(cs: CoroutineScope, dir: String): JComponent = SkillsSettingsUi(cs, dir)
|
||||
override fun update(ui: JComponent, dir: String) {
|
||||
(ui as? SkillsSettingsUi)?.setDirectory(dir)
|
||||
}
|
||||
override fun scrollReadyShell() = false
|
||||
|
||||
companion object { const val ID = "ai.kilocode.jetbrains.settings.agentBehavior.skills" }
|
||||
}
|
||||
|
||||
internal class SkillsSettingsUi(
|
||||
scope: CoroutineScope,
|
||||
dir: String,
|
||||
private val choose: (JComponent) -> String? = ::chooseSkillPath,
|
||||
private val input: (String, String) -> String? = ::inputSkillUrl,
|
||||
private val edit: (SkillDto, Boolean) -> SkillEditDialogHandle = ::SkillEditDialog,
|
||||
) : SettingsListPanel(scope, SettingsListConfig.Equal.copy(tooltip = false)), SettingsDraftPage {
|
||||
private val cs = scope
|
||||
private var dir = dir
|
||||
private var skills = emptyMap<String, SkillDto>()
|
||||
private val app get() = service<KiloAppService>()
|
||||
private val state = SettingsDraftState(skillsDraft(app.state.value.config?.skills ?: SkillsConfigDto()), ::saved)
|
||||
private var draft: SkillsDraft
|
||||
get() = state.draft
|
||||
set(value) {
|
||||
state.draft = value
|
||||
}
|
||||
internal val sources = SkillSourcesView(this, choose, input)
|
||||
|
||||
init {
|
||||
start()
|
||||
setCenter(skillScroll())
|
||||
content.add(sources, BorderLayout.SOUTH)
|
||||
}
|
||||
|
||||
fun setDirectory(value: String) {
|
||||
if (value == dir) return
|
||||
dir = value
|
||||
reload()
|
||||
}
|
||||
|
||||
override suspend fun fetch(): List<SettingsListItem> {
|
||||
val items = withTimeoutOrNull(SKILL_LOAD_TIMEOUT_MS) {
|
||||
service<KiloAgentBehaviorService>().loadSkills(dir)
|
||||
} ?: throw SettingsMessageException(KiloBundle.message("settings.agentBehavior.skills.load.timeout"))
|
||||
withContext(edt) {
|
||||
val dirty = state.modified()
|
||||
val edit = draft
|
||||
state.accept(skillsDraft(config()))
|
||||
if (dirty) draft = state.draft.copy(edited = edit.edited, deleted = edit.deleted)
|
||||
skills = items.associateBy { key(it) }
|
||||
sources.refresh(draft.sources)
|
||||
}
|
||||
LOG.info("skills settings fetch dir=$dir total=${items.size}")
|
||||
return rows(items)
|
||||
}
|
||||
|
||||
override fun afterApply() {
|
||||
sources.refresh(draft.sources)
|
||||
}
|
||||
|
||||
override fun onCell(key: String, cellId: String) {
|
||||
val skill = skills[key] ?: return
|
||||
when (cellId) {
|
||||
OPEN_CELL -> open(skill)
|
||||
EDIT_CELL -> edit(skill)
|
||||
DELETE_CELL -> remove(skill)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchPlaceholder() = KiloBundle.message("settings.agentBehavior.skills.search")
|
||||
|
||||
override fun emptyText() = KiloBundle.message("settings.agentBehavior.skills.empty")
|
||||
|
||||
internal fun updateSources(paths: List<String>, urls: List<String>) {
|
||||
state.update { copy(sources = SkillsConfigDto(paths = paths, urls = urls)) }
|
||||
sources.refresh(draft.sources)
|
||||
}
|
||||
|
||||
override fun modified(): Boolean = state.modified()
|
||||
|
||||
override fun resetDraft() {
|
||||
state.reset()
|
||||
sources.refresh(draft.sources)
|
||||
view.update(rows())
|
||||
clearProgress()
|
||||
}
|
||||
|
||||
override fun applyDraft() {
|
||||
val token = state.start() ?: return
|
||||
val fallback = skillFallback(token.target)
|
||||
if (!launch("apply") { id ->
|
||||
val target = token.target
|
||||
var failed: String? = null
|
||||
val behavior = service<KiloAgentBehaviorService>()
|
||||
LOG.info("skills settings apply start dir=$dir edited=${target.edited.size} deleted=${target.deleted.size} paths=${target.sources.paths.size} urls=${target.sources.urls.size}")
|
||||
if (target.edited.isNotEmpty() && !behavior.saveSkills(dir, target.edited)) {
|
||||
failed = KiloBundle.message("settings.agentBehavior.save.failed")
|
||||
}
|
||||
if (failed == null) {
|
||||
for (location in target.deleted) {
|
||||
if (!behavior.removeSkill(dir, location)) {
|
||||
failed = KiloBundle.message("settings.agentBehavior.skills.delete.failed")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failed == null && target.sources != token.previous.sources) {
|
||||
val patch = ConfigPatchDto(skills = SkillsPatchDto(paths = target.sources.paths, urls = target.sources.urls))
|
||||
if (app.updateConfig(patch) == null) failed = KiloBundle.message("settings.agentBehavior.save.failed")
|
||||
}
|
||||
val reloaded = if (failed == null) behavior.reloadSkills(dir) else true
|
||||
val items = behavior.refreshSkills(dir, fallback)
|
||||
withContext(edt) {
|
||||
if (!active(id)) {
|
||||
if (failed == null) KiloNotifications.info(KiloBundle.message("settings.agentBehavior.skills.saved.notification"))
|
||||
else KiloNotifications.error(failed)
|
||||
return@withContext
|
||||
}
|
||||
if (failed == null) {
|
||||
skills = items.associateBy { key(it) }
|
||||
val next = skillsDraft(config())
|
||||
state.complete(token, next)
|
||||
sources.refresh(draft.sources)
|
||||
view.update(rows(items))
|
||||
if (reloaded) clearProgress() else showProgress(KiloBundle.message("settings.agentBehavior.skills.reload.blocked"))
|
||||
LOG.info("skills settings apply succeeded dir=$dir")
|
||||
} else {
|
||||
state.fail(token, failed)
|
||||
sources.refresh(draft.sources)
|
||||
view.update(rows(items))
|
||||
showError(failed)
|
||||
LOG.warn("skills settings apply failed dir=$dir message=$failed")
|
||||
}
|
||||
setBusy(false)
|
||||
}
|
||||
}) return
|
||||
showProgress(KiloBundle.message("settings.agentBehavior.saving"))
|
||||
}
|
||||
|
||||
private fun skillScroll() = JBScrollPane(view).apply {
|
||||
border = null
|
||||
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||
verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
|
||||
}
|
||||
|
||||
private fun rows(items: List<SkillDto> = skills.values.toList()): List<SettingsListItem> = items.mapNotNull { skill ->
|
||||
if (skill.location in draft.deleted) return@mapNotNull null
|
||||
item(skill)
|
||||
}
|
||||
|
||||
private fun skillFallback(target: SkillsDraft): List<SkillDto> = skills.values.mapNotNull { skill ->
|
||||
if (skill.location in target.deleted) return@mapNotNull null
|
||||
target.edited[skill.location]?.let { skill.copy(content = it) } ?: skill
|
||||
}
|
||||
|
||||
private fun item(skill: SkillDto) = object : SettingsListItem {
|
||||
override val key = key(skill)
|
||||
override val title = skill.name
|
||||
override val note = skill.location.takeUnless { builtin(it) }
|
||||
override val description = skill.description
|
||||
override val doubleClick = EDIT_CELL
|
||||
override val badges = listOf(
|
||||
SettingsBadge(KiloBundle.message("settings.agentBehavior.badge.builtin"), UiStyle.Badge.Secondary),
|
||||
).takeIf { builtin(skill.location) } ?: emptyList()
|
||||
override val cells = listOfNotNull(
|
||||
SettingsListCell(
|
||||
OPEN_CELL,
|
||||
KiloBundle.message("settings.agentBehavior.skills.openInEditor"),
|
||||
primary = true,
|
||||
).takeIf { skill.editable },
|
||||
SettingsListCell(
|
||||
EDIT_CELL,
|
||||
KiloBundle.message(if (skill.editable) "settings.agentBehavior.edit" else "common.open"),
|
||||
primary = !skill.editable,
|
||||
),
|
||||
SettingsListCell(
|
||||
DELETE_CELL,
|
||||
KiloBundle.message("common.delete"),
|
||||
icon = AllIcons.Actions.GC,
|
||||
iconOnly = true,
|
||||
).takeIf { skill.editable },
|
||||
)
|
||||
}
|
||||
|
||||
private fun edit(skill: SkillDto) {
|
||||
val current = skill.copy(content = content(skill))
|
||||
val dialog = edit(current, skill.editable)
|
||||
if (!skill.editable) {
|
||||
dialog.showAndGet()
|
||||
return
|
||||
}
|
||||
if (!dialog.showAndGet()) return
|
||||
state.update { copy(edited = edited + (skill.location to dialog.content())) }
|
||||
view.update(rows(), SettingsListSelection.Key(key(skill)))
|
||||
}
|
||||
|
||||
private fun open(skill: SkillDto) {
|
||||
if (!skill.editable) return
|
||||
showProgress(KiloBundle.message("settings.agentBehavior.skills.openInEditor.pending"))
|
||||
cs.launch {
|
||||
val opened = service<KiloWorkspaceService>().openFile(skill.location)
|
||||
if (opened) return@launch
|
||||
withContext(edt) { KiloNotifications.error(KiloBundle.message("settings.agentBehavior.skills.openInEditor.failed")) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun remove(skill: SkillDto) {
|
||||
val result = Messages.showYesNoDialog(
|
||||
KiloBundle.message("settings.agentBehavior.skills.delete.message", skill.name),
|
||||
KiloBundle.message("settings.agentBehavior.skills.delete.title"),
|
||||
KiloBundle.message("common.delete"),
|
||||
Messages.getCancelButton(),
|
||||
Messages.getQuestionIcon(),
|
||||
)
|
||||
if (result != Messages.YES) return
|
||||
state.update { copy(deleted = deleted + skill.location, edited = edited - skill.location) }
|
||||
view.update(rows(), selectionIndex())
|
||||
}
|
||||
|
||||
private fun content(skill: SkillDto) = draft.edited[skill.location] ?: skill.content
|
||||
|
||||
private fun config() = app.state.value.config?.skills ?: SkillsConfigDto()
|
||||
|
||||
private companion object {
|
||||
const val EDIT_CELL = "edit"
|
||||
const val OPEN_CELL = "open"
|
||||
const val DELETE_CELL = "delete"
|
||||
const val BUILTIN = "builtin"
|
||||
const val LEGACY_BUILTIN = "<built-in>"
|
||||
val LOG = KiloLog.create(SkillsSettingsUi::class.java)
|
||||
|
||||
fun key(skill: SkillDto) = skill.location.ifBlank { skill.name }
|
||||
fun builtin(location: String) = location == BUILTIN || location == LEGACY_BUILTIN
|
||||
}
|
||||
}
|
||||
|
||||
internal interface SkillEditDialogHandle {
|
||||
fun showAndGet(): Boolean
|
||||
fun content(): String
|
||||
}
|
||||
|
||||
private data class SkillsDraft(
|
||||
val sources: SkillsConfigDto,
|
||||
val edited: Map<String, String> = emptyMap(),
|
||||
val deleted: Set<String> = emptySet(),
|
||||
)
|
||||
|
||||
private fun skillsDraft(sources: SkillsConfigDto) = SkillsDraft(sources)
|
||||
|
||||
private fun saved(base: SkillsDraft, draft: SkillsDraft): Boolean = base == draft
|
||||
|
||||
internal class SkillEditDialog(private val skill: SkillDto, private val savable: Boolean) : DialogWrapper(true), SkillEditDialogHandle {
|
||||
private val base = initial()
|
||||
private val editor = SkillEditor(base, skill.location, savable)
|
||||
|
||||
init {
|
||||
title = skill.name
|
||||
setOKButtonText(CommonBundle.getOkButtonText())
|
||||
setCancelButtonText(CommonBundle.getCloseButtonText())
|
||||
init()
|
||||
isOKActionEnabled = false
|
||||
editor.document.addDocumentListener(object : DocumentListener {
|
||||
override fun documentChanged(event: DocumentEvent) {
|
||||
isOKActionEnabled = savable && editor.text != base
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent = JBScrollPane(editor).apply {
|
||||
viewportBorder = editorPad()
|
||||
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||
verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
|
||||
preferredSize = JBUI.size(720, 520)
|
||||
}
|
||||
|
||||
override fun createActions() = if (savable) arrayOf(okAction, cancelAction) else arrayOf(cancelAction)
|
||||
|
||||
override fun content() = editor.text
|
||||
|
||||
private fun initial() = skill.content?.takeIf { it.isNotBlank() }
|
||||
?: skill.description?.takeIf { it.isNotBlank() }
|
||||
?: KiloBundle.message("settings.agentBehavior.skills.content.empty")
|
||||
|
||||
private class SkillEditor(value: String, location: String, editable: Boolean) : EditorTextField(
|
||||
EditorFactory.getInstance().createDocument(value),
|
||||
ProjectManager.getInstance().defaultProject,
|
||||
skillFileType(location, value),
|
||||
false,
|
||||
!editable,
|
||||
) {
|
||||
init {
|
||||
border = JBUI.Borders.empty()
|
||||
setOneLineMode(false)
|
||||
addSettingsProvider { ed ->
|
||||
ed.setBorder(JBUI.Borders.empty())
|
||||
ed.scrollPane.border = JBUI.Borders.empty()
|
||||
ed.scrollPane.viewportBorder = JBUI.Borders.empty()
|
||||
ed.settings.isUseSoftWraps = true
|
||||
ed.settings.isPaintSoftWraps = false
|
||||
ed.settings.isAdditionalPageAtBottom = false
|
||||
ed.scrollPane.horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||
ed.scrollPane.verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class SkillSourcesView(
|
||||
private val parent: SkillsSettingsUi,
|
||||
private val choose: (JComponent) -> String?,
|
||||
private val input: (String, String) -> String?,
|
||||
) : Stack(ai.kilocode.client.ui.layout.StackAxis.VERTICAL, UiStyle.Gap.sm()) {
|
||||
private val view = SettingsListView(
|
||||
KiloBundle.message("settings.agentBehavior.skills.sources.empty"),
|
||||
SettingsListConfig.Preferred.copy(description = false, selection = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION),
|
||||
) { key, id ->
|
||||
if (id == EDIT_CELL) edit(key)
|
||||
}
|
||||
private var cfg = SkillsConfigDto()
|
||||
|
||||
internal fun sourceList() = view.list
|
||||
|
||||
init {
|
||||
border = JBUI.Borders.empty(UiStyle.Gap.pad(), 0, 0, 0)
|
||||
next(TitledSeparator(KiloBundle.message("settings.agentBehavior.skills.sources.title")))
|
||||
next(toolbar())
|
||||
next(JBScrollPane(view).apply {
|
||||
border = null
|
||||
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||
preferredSize = JBUI.size(0, JBUI.scale(160))
|
||||
maximumSize = JBUI.size(Int.MAX_VALUE, JBUI.scale(160))
|
||||
})
|
||||
}
|
||||
|
||||
fun refresh(config: SkillsConfigDto) {
|
||||
cfg = config
|
||||
view.update(rows(config))
|
||||
}
|
||||
|
||||
private fun toolbar(): JComponent {
|
||||
val add = DefaultActionGroup(KiloBundle.message("settings.agentBehavior.skills.sources.add"), true).apply {
|
||||
templatePresentation.icon = AllIcons.General.Add
|
||||
add(AddPathAction())
|
||||
add(AddUrlAction())
|
||||
}
|
||||
val group = DefaultActionGroup(add, RemoveAction())
|
||||
val toolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.TOOLBAR, group, true)
|
||||
toolbar.targetComponent = this
|
||||
toolbar.updateActionsImmediately()
|
||||
return toolbar.component
|
||||
}
|
||||
|
||||
internal fun addPath() {
|
||||
val path = choose(parent)?.trim()?.takeIf { it.isNotBlank() } ?: return
|
||||
if (path in cfg.paths) return
|
||||
parent.updateSources(cfg.paths + path, cfg.urls)
|
||||
}
|
||||
|
||||
internal fun addUrl() {
|
||||
val url = input(
|
||||
KiloBundle.message("settings.agentBehavior.skills.sources.addUrl.title"),
|
||||
KiloBundle.message("settings.agentBehavior.skills.sources.addUrl.prompt"),
|
||||
)?.trim()?.takeIf { it.isNotBlank() } ?: return
|
||||
if (url in cfg.urls) return
|
||||
parent.updateSources(cfg.paths, cfg.urls + url)
|
||||
}
|
||||
|
||||
private fun rows(config: SkillsConfigDto): List<SettingsListItem> {
|
||||
val paths = config.paths.map { source(PATH_PREFIX, it) }
|
||||
val urls = config.urls.map { source(URL_PREFIX, it) }
|
||||
return paths + urls
|
||||
}
|
||||
|
||||
private fun source(prefix: String, value: String) = object : SettingsListItem {
|
||||
override val key = prefix + value
|
||||
override val title = value
|
||||
override val doubleClick = EDIT_CELL
|
||||
}
|
||||
|
||||
internal fun removeSelected() {
|
||||
val keys = view.selectedItems().map { it.key }.toSet()
|
||||
if (keys.isEmpty()) return
|
||||
val paths = cfg.paths.filterNot { PATH_PREFIX + it in keys }
|
||||
val urls = cfg.urls.filterNot { URL_PREFIX + it in keys }
|
||||
parent.updateSources(paths, urls)
|
||||
}
|
||||
|
||||
private fun edit(key: String) {
|
||||
val path = key.startsWith(PATH_PREFIX)
|
||||
val old = key.removePrefix(if (path) PATH_PREFIX else URL_PREFIX)
|
||||
val dialog = SkillSourceDialog(old, path, choose)
|
||||
if (!dialog.showAndGet()) return
|
||||
val next = dialog.value().trim().takeIf { it.isNotBlank() } ?: return
|
||||
if (path) {
|
||||
parent.updateSources(cfg.paths.map { if (it == old) next else it }.distinct(), cfg.urls)
|
||||
return
|
||||
}
|
||||
parent.updateSources(cfg.paths, cfg.urls.map { if (it == old) next else it }.distinct())
|
||||
}
|
||||
|
||||
private inner class AddPathAction : DumbAwareAction(
|
||||
KiloBundle.message("settings.agentBehavior.skills.sources.addPath"),
|
||||
null,
|
||||
null,
|
||||
) {
|
||||
override fun getActionUpdateThread() = ActionUpdateThread.EDT
|
||||
override fun actionPerformed(e: AnActionEvent) = addPath()
|
||||
}
|
||||
|
||||
private inner class AddUrlAction : DumbAwareAction(
|
||||
KiloBundle.message("settings.agentBehavior.skills.sources.addUrl"),
|
||||
null,
|
||||
null,
|
||||
) {
|
||||
override fun getActionUpdateThread() = ActionUpdateThread.EDT
|
||||
override fun actionPerformed(e: AnActionEvent) = addUrl()
|
||||
}
|
||||
|
||||
private inner class RemoveAction : DumbAwareAction(
|
||||
KiloBundle.message("common.delete"),
|
||||
null,
|
||||
AllIcons.General.Remove,
|
||||
) {
|
||||
override fun getActionUpdateThread() = ActionUpdateThread.EDT
|
||||
override fun update(e: AnActionEvent) {
|
||||
e.presentation.isEnabled = view.selectedItems().isNotEmpty()
|
||||
}
|
||||
override fun actionPerformed(e: AnActionEvent) = removeSelected()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val EDIT_CELL = "edit"
|
||||
const val PATH_PREFIX = "path:"
|
||||
const val URL_PREFIX = "url:"
|
||||
}
|
||||
}
|
||||
|
||||
private class SkillSourceDialog(
|
||||
value: String,
|
||||
private val path: Boolean,
|
||||
private val choose: (JComponent) -> String?,
|
||||
) : DialogWrapper(true) {
|
||||
private val field = JBTextField(value)
|
||||
|
||||
init {
|
||||
title = if (path) KiloBundle.message("settings.agentBehavior.skills.sources.editPath.title")
|
||||
else KiloBundle.message("settings.agentBehavior.skills.sources.editUrl.title")
|
||||
setOKButtonText(KiloBundle.message("common.save"))
|
||||
init()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
if (!path) return field.apply { columns = SOURCE_COLUMNS }
|
||||
return JPanel(BorderLayout(UiStyle.Gap.sm(), 0)).apply {
|
||||
add(field.apply { columns = SOURCE_COLUMNS }, BorderLayout.CENTER)
|
||||
add(JButton("...").apply {
|
||||
addActionListener {
|
||||
choose(this)?.let { field.text = it }
|
||||
}
|
||||
}, BorderLayout.EAST)
|
||||
}
|
||||
}
|
||||
|
||||
fun value() = field.text
|
||||
}
|
||||
|
||||
private fun chooseSkillPath(parent: JComponent): String? {
|
||||
return FileChooser.chooseFile(skillPathDescriptor(), parent, null, null as VirtualFile?)?.path
|
||||
}
|
||||
|
||||
internal fun skillPathDescriptor() = FileChooserDescriptor(false, true, false, false, false, false).apply {
|
||||
title = KiloBundle.message("settings.agentBehavior.skills.sources.addPath.title")
|
||||
description = KiloBundle.message("settings.agentBehavior.skills.sources.addPath.prompt")
|
||||
}
|
||||
|
||||
internal fun skillFileType(location: String, content: String? = null): FileType {
|
||||
val syntax = content?.syntaxName()
|
||||
val name = syntax ?: location.substringAfterLast('/').substringAfterLast('\\').ifBlank { SKILL_FILE }
|
||||
val type = FileTypeManager.getInstance().getFileTypeByFileName(name)
|
||||
if (type == UnknownFileType.INSTANCE) return PlainTextFileType.INSTANCE
|
||||
return type
|
||||
}
|
||||
|
||||
private fun String.syntaxName(): String? {
|
||||
val text = trimStart()
|
||||
if (text.isBlank()) return null
|
||||
if (text.looksHtml()) return "index.html"
|
||||
if (text.looksMarkdown()) return SKILL_FILE
|
||||
return null
|
||||
}
|
||||
|
||||
private fun String.looksHtml() = contains(Regex("^\\s*(<!doctype\\s+html|<html\\b|<body\\b|</?(h[1-6]|p|pre|code|ul|ol|li|blockquote|br)\\b)", RegexOption.IGNORE_CASE))
|
||||
|
||||
private fun String.looksMarkdown() = lineSequence().any { line ->
|
||||
line.matches(Regex("\\s{0,3}(#{1,6}\\s+.+|[-*+]\\s+.+|\\d+\\.\\s+.+|```.*|>\\s+.+)")) ||
|
||||
line.contains(Regex("(`[^`]+`|\\[[^]]+][(][^)]+[)])"))
|
||||
}
|
||||
|
||||
private fun inputSkillUrl(title: String, prompt: String): String? = Messages.showInputDialog(
|
||||
prompt,
|
||||
title,
|
||||
Messages.getQuestionIcon(),
|
||||
)
|
||||
|
||||
private fun editorPad() = JBUI.Borders.empty(
|
||||
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_HORIZONTAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_VERTICAL_PADDING),
|
||||
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_HORIZONTAL_PADDING),
|
||||
)
|
||||
|
||||
private const val SOURCE_COLUMNS = 60
|
||||
private const val SKILL_FILE = "SKILL.md"
|
||||
private const val SKILL_LOAD_TIMEOUT_MS = 10_000L
|
||||
+5
@@ -8,6 +8,7 @@ import java.awt.Point
|
||||
import java.awt.Rectangle
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JList
|
||||
import javax.swing.ListSelectionModel
|
||||
import javax.swing.ListCellRenderer
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
@@ -21,6 +22,8 @@ internal data class SettingsListConfig(
|
||||
val height: SettingsListRowHeight,
|
||||
val description: Boolean = true,
|
||||
val descriptionIndent: Boolean = true,
|
||||
val tooltip: Boolean = true,
|
||||
val selection: Int = ListSelectionModel.SINGLE_SELECTION,
|
||||
) {
|
||||
companion object {
|
||||
val Equal = SettingsListConfig(SettingsListRowHeight.EQUAL)
|
||||
@@ -41,7 +44,9 @@ internal data class SettingsListCell(
|
||||
internal interface SettingsListItem {
|
||||
val key: String
|
||||
val title: String
|
||||
val note: String? get() = null
|
||||
val description: String? get() = null
|
||||
val doubleClick: String? get() = null
|
||||
val icon: Icon? get() = null
|
||||
val section: String? get() = null
|
||||
val badges: List<SettingsBadge> get() = emptyList()
|
||||
|
||||
+3
@@ -83,6 +83,9 @@ internal class SettingsListRenderer(
|
||||
|
||||
title.clear()
|
||||
title.append(value.title, SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, fg))
|
||||
value.note?.takeIf { it.isNotBlank() }?.let {
|
||||
title.append(" $it", SimpleTextAttributes.GRAYED_ATTRIBUTES)
|
||||
}
|
||||
syncBadges(value)
|
||||
icon.icon = value.icon
|
||||
mark.isVisible = value.icon != null
|
||||
|
||||
+51
-7
@@ -1,6 +1,7 @@
|
||||
package ai.kilocode.client.settings.base
|
||||
|
||||
import ai.kilocode.client.session.ui.model.ModelSearch
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.ui.CollectionListModel
|
||||
import com.intellij.ui.ScrollingUtil
|
||||
@@ -8,23 +9,27 @@ import com.intellij.ui.components.JBList
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.xml.util.XmlStringUtil
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import java.awt.Dimension
|
||||
import java.awt.Rectangle
|
||||
import java.awt.event.KeyEvent
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.KeyStroke
|
||||
import javax.swing.ListSelectionModel
|
||||
import javax.swing.Scrollable
|
||||
import javax.swing.SwingConstants
|
||||
import javax.swing.event.ListSelectionEvent
|
||||
|
||||
internal class SettingsListView(
|
||||
empty: String,
|
||||
private val cfg: SettingsListConfig = SettingsListConfig.Equal,
|
||||
private val onCell: (String, String) -> Unit,
|
||||
) : BaseContentPanel() {
|
||||
) : BaseContentPanel(), Scrollable {
|
||||
private val model = CollectionListModel<SettingsListItem>()
|
||||
internal val list = object : JBList<SettingsListItem>(model) {
|
||||
override fun getToolTipText(event: MouseEvent): String? {
|
||||
if (!cfg.description) return null
|
||||
if (!cfg.description || !cfg.tooltip) return null
|
||||
val idx = locationToIndex(event.point)
|
||||
if (idx < 0) return null
|
||||
val bounds = getCellBounds(idx, idx) ?: return null
|
||||
@@ -34,7 +39,7 @@ internal class SettingsListView(
|
||||
return XmlStringUtil.wrapInHtml(text)
|
||||
}
|
||||
}.apply {
|
||||
selectionMode = ListSelectionModel.SINGLE_SELECTION
|
||||
selectionMode = cfg.selection
|
||||
setExpandableItemsEnabled(false)
|
||||
emptyText.text = empty
|
||||
}
|
||||
@@ -67,6 +72,11 @@ internal class SettingsListView(
|
||||
val hit = hit(e, enabled = false) ?: return
|
||||
if (hit.id != null) return
|
||||
val item = hit.item
|
||||
item.doubleClick?.let { id ->
|
||||
onCell(item.key, id)
|
||||
e.consume()
|
||||
return
|
||||
}
|
||||
primary(item)
|
||||
e.consume()
|
||||
}
|
||||
@@ -94,6 +104,12 @@ internal class SettingsListView(
|
||||
return list.selectedValue
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun selectedItems(): List<SettingsListItem> {
|
||||
checkEdt()
|
||||
return list.selectedValuesList
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun selectedIndex(): Int {
|
||||
checkEdt()
|
||||
@@ -121,6 +137,7 @@ internal class SettingsListView(
|
||||
@RequiresEdt
|
||||
fun setBusy(value: Boolean) {
|
||||
checkEdt()
|
||||
list.setPaintBusy(value)
|
||||
if (list.isEnabled == !value) return
|
||||
list.isEnabled = !value
|
||||
list.repaint()
|
||||
@@ -191,9 +208,15 @@ internal class SettingsListView(
|
||||
private fun primary(item: SettingsListItem) {
|
||||
val cells = settingsListVisibleCells(item, true)
|
||||
val cell = cells.firstOrNull { it.enabled && it.primary }
|
||||
?: cells.firstOrNull { it.enabled }
|
||||
?: return
|
||||
onCell(item.key, cell.id)
|
||||
if (cell != null) {
|
||||
onCell(item.key, cell.id)
|
||||
return
|
||||
}
|
||||
item.doubleClick?.let { id ->
|
||||
onCell(item.key, id)
|
||||
return
|
||||
}
|
||||
cells.firstOrNull { it.enabled }?.let { onCell(item.key, it.id) }
|
||||
}
|
||||
|
||||
private fun hit(e: MouseEvent, enabled: Boolean = true): Hit? {
|
||||
@@ -201,7 +224,7 @@ internal class SettingsListView(
|
||||
val bounds = idx.takeIf { it >= 0 }?.let { list.getCellBounds(it, it) } ?: return null
|
||||
if (!bounds.contains(e.point)) return null
|
||||
val item = model.getElementAt(idx)
|
||||
val selected = idx == list.selectedIndex
|
||||
val selected = list.isSelectedIndex(idx)
|
||||
val id = if (enabled) {
|
||||
settingsListCellAt(list, idx, e.point, selected)
|
||||
} else {
|
||||
@@ -217,6 +240,27 @@ internal class SettingsListView(
|
||||
check(ApplicationManager.getApplication().isDispatchThread) { "Settings list updates must run on EDT" }
|
||||
}
|
||||
|
||||
override fun getScrollableTracksViewportWidth() = true
|
||||
|
||||
override fun getScrollableTracksViewportHeight() = false
|
||||
|
||||
override fun getPreferredScrollableViewportSize(): Dimension = preferredSize
|
||||
|
||||
override fun getScrollableUnitIncrement(
|
||||
visibleRect: Rectangle,
|
||||
orientation: Int,
|
||||
direction: Int,
|
||||
): Int {
|
||||
if (orientation != SwingConstants.VERTICAL) return UiStyle.Gap.pad()
|
||||
return list.fixedCellHeight.takeIf { it > 0 } ?: UiStyle.Gap.xl()
|
||||
}
|
||||
|
||||
override fun getScrollableBlockIncrement(
|
||||
visibleRect: Rectangle,
|
||||
orientation: Int,
|
||||
direction: Int,
|
||||
) = if (orientation == SwingConstants.VERTICAL) visibleRect.height else visibleRect.width
|
||||
|
||||
private data class Hit(val item: SettingsListItem, val id: String?)
|
||||
|
||||
private data class Press(val key: String, val id: String)
|
||||
|
||||
+8
@@ -33,6 +33,14 @@ internal open class SettingsPanel : SettingsOverlayPanel() {
|
||||
repaint()
|
||||
}
|
||||
|
||||
protected fun setCenter(component: JComponent) {
|
||||
val layout = content.layout as? BorderLayout
|
||||
layout?.getLayoutComponent(BorderLayout.CENTER)?.let { content.remove(it) }
|
||||
content.add(component, BorderLayout.CENTER)
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class SettingsBody : Stack(StackAxis.VERTICAL), Scrollable {
|
||||
|
||||
@@ -84,6 +84,13 @@
|
||||
bundle="messages.KiloBundle"
|
||||
key="settings.agentBehavior.mcp.displayName"/>
|
||||
|
||||
<applicationConfigurable
|
||||
parentId="ai.kilocode.jetbrains.settings.agentBehavior"
|
||||
id="ai.kilocode.jetbrains.settings.agentBehavior.skills"
|
||||
instance="ai.kilocode.client.settings.agents.SkillsConfigurable"
|
||||
bundle="messages.KiloBundle"
|
||||
key="settings.agentBehavior.skills.displayName"/>
|
||||
|
||||
<registryKey key="kilo.session.condense"
|
||||
description="Enable event condensing in the session update queue (merges redundant snapshots before model delivery)."
|
||||
defaultValue="true"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
common.delete=Delete
|
||||
common.open=Open
|
||||
common.save=Save
|
||||
session.action.cancel=Cancel
|
||||
|
||||
session.connection.connecting=Loading...
|
||||
@@ -457,6 +459,31 @@ settings.agentBehavior.mcp.status.failed=failed
|
||||
settings.agentBehavior.mcp.status.needsAuth=needs auth
|
||||
settings.agentBehavior.mcp.status.needsRegistration=needs registration
|
||||
settings.agentBehavior.mcp.status.disabled=disabled
|
||||
settings.agentBehavior.skills.displayName=Skills
|
||||
settings.agentBehavior.skills.search=Filter skills
|
||||
settings.agentBehavior.skills.empty=No skills found.
|
||||
settings.agentBehavior.skills.content.empty=No skill content available.
|
||||
settings.agentBehavior.skills.load.timeout=Skill loading timed out. Existing skills were kept; remove slow or unreachable URLs and refresh.
|
||||
settings.agentBehavior.skills.reload.deferred=Skills source saved. Reload the core after active sessions finish to apply new skills.
|
||||
settings.agentBehavior.skills.reload.blocked=Skills settings saved, but active sessions are present. Reload the core after those sessions finish to apply the new skills.
|
||||
settings.agentBehavior.skills.saved.notification=Skills settings saved
|
||||
settings.agentBehavior.skills.delete.title=Delete Skill
|
||||
settings.agentBehavior.skills.delete.message=Delete skill {0}? This removes the skill file and cannot be undone.
|
||||
settings.agentBehavior.skills.delete.failed=Could not delete the skill.
|
||||
settings.agentBehavior.skills.openInEditor=Open in Editor
|
||||
settings.agentBehavior.skills.openInEditor.pending=The skill file will open after you close Settings.
|
||||
settings.agentBehavior.skills.openInEditor.failed=Could not open the skill file in the editor.
|
||||
settings.agentBehavior.skills.sources.empty=No skill sources configured.
|
||||
settings.agentBehavior.skills.sources.title=Additional Skill Sources
|
||||
settings.agentBehavior.skills.sources.add=Add
|
||||
settings.agentBehavior.skills.sources.addPath=Add path
|
||||
settings.agentBehavior.skills.sources.addPath.title=Add Skill Path
|
||||
settings.agentBehavior.skills.sources.addPath.prompt=Choose a folder containing Kilo skills.
|
||||
settings.agentBehavior.skills.sources.addUrl=Add URL
|
||||
settings.agentBehavior.skills.sources.addUrl.title=Add Skill URL
|
||||
settings.agentBehavior.skills.sources.addUrl.prompt=Enter a skill source URL.
|
||||
settings.agentBehavior.skills.sources.editPath.title=Edit Skill Path
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Edit Skill URL
|
||||
settings.providers.loading=Loading providers...
|
||||
settings.providers.connected=Connected providers
|
||||
settings.providers.available=Available providers
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
common.delete=حذف
|
||||
common.open=فتح
|
||||
common.save=حفظ
|
||||
session.action.cancel=إلغاء
|
||||
session.connection.connecting=جاري التحميل…
|
||||
session.connection.error.app=فشل الاتصال
|
||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
||||
|
||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||
settings.agentBehavior.mcp.addHint=لإضافة خادم MCP، اطلب من الوكيل إضافته.
|
||||
settings.agentBehavior.skills.displayName=المهارات
|
||||
settings.agentBehavior.skills.search=تصفية المهارات
|
||||
settings.agentBehavior.skills.empty=لم يتم العثور على مهارات.
|
||||
settings.agentBehavior.skills.content.empty=لا يوجد محتوى مهارة متاح.
|
||||
settings.agentBehavior.skills.load.timeout=انتهت مهلة تحميل المهارات. تم الاحتفاظ بالمهارات الحالية؛ أزل عناوين URL البطيئة أو غير المتاحة ثم حدّث.
|
||||
settings.agentBehavior.skills.reload.deferred=تم حفظ مصدر المهارات. أعد تحميل Core بعد انتهاء الجلسات النشطة لتطبيق المهارات الجديدة.
|
||||
settings.agentBehavior.skills.reload.blocked=تم حفظ إعدادات المهارات، لكن توجد جلسات نشطة. أعد تحميل Core بعد انتهاء تلك الجلسات لتطبيق المهارات الجديدة.
|
||||
settings.agentBehavior.skills.saved.notification=تم حفظ إعدادات المهارات
|
||||
settings.agentBehavior.skills.delete.title=حذف المهارة
|
||||
settings.agentBehavior.skills.delete.message=هل تريد حذف المهارة {0}؟ سيؤدي ذلك إلى إزالة ملف المهارة ولا يمكن التراجع عنه.
|
||||
settings.agentBehavior.skills.delete.failed=تعذر حذف المهارة.
|
||||
settings.agentBehavior.skills.openInEditor=فتح في المحرر
|
||||
settings.agentBehavior.skills.openInEditor.pending=سيتم فتح ملف المهارة بعد إغلاق الإعدادات.
|
||||
settings.agentBehavior.skills.openInEditor.failed=تعذر فتح ملف المهارة في المحرر.
|
||||
settings.agentBehavior.skills.sources.empty=لم يتم تكوين مصادر مهارات.
|
||||
settings.agentBehavior.skills.sources.title=مصادر مهارات إضافية
|
||||
settings.agentBehavior.skills.sources.add=إضافة
|
||||
settings.agentBehavior.skills.sources.addPath=إضافة مسار
|
||||
settings.agentBehavior.skills.sources.addPath.title=إضافة مسار مهارات
|
||||
settings.agentBehavior.skills.sources.addPath.prompt=اختر مجلداً يحتوي على مهارات Kilo.
|
||||
settings.agentBehavior.skills.sources.addUrl=إضافة URL
|
||||
settings.agentBehavior.skills.sources.addUrl.title=إضافة URL لمصدر مهارات
|
||||
settings.agentBehavior.skills.sources.addUrl.prompt=أدخل URL لمصدر مهارات.
|
||||
settings.agentBehavior.skills.sources.editPath.title=تعديل مسار المهارات
|
||||
settings.agentBehavior.skills.sources.editUrl.title=تعديل URL المهارات
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.count.one=تم التراجع عن رسالة واحدة
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
common.delete=Obriši
|
||||
common.open=Otvori
|
||||
common.save=Sačuvaj
|
||||
session.action.cancel=Otkaži
|
||||
session.connection.connecting=Učitavanje…
|
||||
session.connection.error.app=Greška pri spajanju
|
||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
||||
|
||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||
settings.agentBehavior.mcp.addHint=Da dodate MCP server, zamolite agenta da ga doda.
|
||||
settings.agentBehavior.skills.displayName=Vještine
|
||||
settings.agentBehavior.skills.search=Filtriraj vještine
|
||||
settings.agentBehavior.skills.empty=Nema pronađenih vještina.
|
||||
settings.agentBehavior.skills.content.empty=Nema dostupnog sadržaja vještine.
|
||||
settings.agentBehavior.skills.load.timeout=Učitavanje vještina je isteklo. Postojeće vještine su zadržane; uklonite spore ili nedostupne URL-ove i osvježite.
|
||||
settings.agentBehavior.skills.reload.deferred=Izvor vještina je sačuvan. Ponovo učitajte Core nakon što aktivne sesije završe da primijenite nove vještine.
|
||||
settings.agentBehavior.skills.reload.blocked=Postavke vještina su sačuvane, ali postoje aktivne sesije. Ponovo učitajte Core nakon što te sesije završe da primijenite nove vještine.
|
||||
settings.agentBehavior.skills.saved.notification=Postavke vještina su sačuvane
|
||||
settings.agentBehavior.skills.delete.title=Izbriši vještinu
|
||||
settings.agentBehavior.skills.delete.message=Izbrisati vještinu {0}? Ovo uklanja datoteku vještine i ne može se poništiti.
|
||||
settings.agentBehavior.skills.delete.failed=Nije moguće izbrisati vještinu.
|
||||
settings.agentBehavior.skills.openInEditor=Otvori u editoru
|
||||
settings.agentBehavior.skills.openInEditor.pending=Datoteka vještine će se otvoriti nakon što zatvorite Postavke.
|
||||
settings.agentBehavior.skills.openInEditor.failed=Nije moguće otvoriti datoteku vještine u editoru.
|
||||
settings.agentBehavior.skills.sources.empty=Nema konfigurisanih izvora vještina.
|
||||
settings.agentBehavior.skills.sources.title=Dodatni izvori vještina
|
||||
settings.agentBehavior.skills.sources.add=Dodaj
|
||||
settings.agentBehavior.skills.sources.addPath=Dodaj putanju
|
||||
settings.agentBehavior.skills.sources.addPath.title=Dodaj putanju vještina
|
||||
settings.agentBehavior.skills.sources.addPath.prompt=Odaberite folder koji sadrži Kilo vještine.
|
||||
settings.agentBehavior.skills.sources.addUrl=Dodaj URL
|
||||
settings.agentBehavior.skills.sources.addUrl.title=Dodaj URL izvora vještina
|
||||
settings.agentBehavior.skills.sources.addUrl.prompt=Unesite URL izvora vještina.
|
||||
settings.agentBehavior.skills.sources.editPath.title=Uredi putanju vještina
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Uredi URL vještina
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.count.one={0} poruka vraćena
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
common.delete=Slet
|
||||
common.open=Åbn
|
||||
common.save=Gem
|
||||
session.action.cancel=Annuller
|
||||
session.connection.connecting=Indlæser…
|
||||
session.connection.error.app=Forbindelsesfejl
|
||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
||||
|
||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||
settings.agentBehavior.mcp.addHint=For at tilføje en MCP-server skal du bede agenten om at gøre det.
|
||||
settings.agentBehavior.skills.displayName=Færdigheder
|
||||
settings.agentBehavior.skills.search=Filtrer færdigheder
|
||||
settings.agentBehavior.skills.empty=Ingen færdigheder fundet.
|
||||
settings.agentBehavior.skills.content.empty=Intet færdighedsindhold tilgængeligt.
|
||||
settings.agentBehavior.skills.load.timeout=Indlæsning af færdigheder fik timeout. Eksisterende færdigheder blev bevaret; fjern langsomme eller utilgængelige URL'er og opdater.
|
||||
settings.agentBehavior.skills.reload.deferred=Færdighedskilden blev gemt. Genindlæs Core, når aktive sessioner er afsluttet, for at anvende nye færdigheder.
|
||||
settings.agentBehavior.skills.reload.blocked=Færdighedsindstillingerne blev gemt, men der er aktive sessioner. Genindlæs Core, når disse sessioner er afsluttet, for at anvende de nye færdigheder.
|
||||
settings.agentBehavior.skills.saved.notification=Færdighedsindstillinger gemt
|
||||
settings.agentBehavior.skills.delete.title=Slet færdighed
|
||||
settings.agentBehavior.skills.delete.message=Slet færdigheden {0}? Dette fjerner færdighedsfilen og kan ikke fortrydes.
|
||||
settings.agentBehavior.skills.delete.failed=Kunne ikke slette færdigheden.
|
||||
settings.agentBehavior.skills.openInEditor=Åbn i editor
|
||||
settings.agentBehavior.skills.openInEditor.pending=Færdighedsfilen åbnes, når du lukker Indstillinger.
|
||||
settings.agentBehavior.skills.openInEditor.failed=Kunne ikke åbne færdighedsfilen i editoren.
|
||||
settings.agentBehavior.skills.sources.empty=Ingen færdighedskilder konfigureret.
|
||||
settings.agentBehavior.skills.sources.title=Yderligere færdighedskilder
|
||||
settings.agentBehavior.skills.sources.add=Tilføj
|
||||
settings.agentBehavior.skills.sources.addPath=Tilføj sti
|
||||
settings.agentBehavior.skills.sources.addPath.title=Tilføj færdighedssti
|
||||
settings.agentBehavior.skills.sources.addPath.prompt=Vælg en mappe, der indeholder Kilo-færdigheder.
|
||||
settings.agentBehavior.skills.sources.addUrl=Tilføj URL
|
||||
settings.agentBehavior.skills.sources.addUrl.title=Tilføj URL til færdighedskilde
|
||||
settings.agentBehavior.skills.sources.addUrl.prompt=Indtast en URL til en færdighedskilde.
|
||||
settings.agentBehavior.skills.sources.editPath.title=Rediger færdighedssti
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Rediger færdigheds-URL
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.count.one={0} besked rullet tilbage
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
common.delete=Löschen
|
||||
common.open=Öffnen
|
||||
common.save=Speichern
|
||||
session.action.cancel=Abbrechen
|
||||
session.connection.connecting=Wird geladen…
|
||||
session.connection.error.app=Verbindung fehlgeschlagen
|
||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
||||
|
||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||
settings.agentBehavior.mcp.addHint=Um einen MCP-Server hinzuzufügen, bitten Sie den Agenten darum.
|
||||
settings.agentBehavior.skills.displayName=Skills
|
||||
settings.agentBehavior.skills.search=Skills filtern
|
||||
settings.agentBehavior.skills.empty=Keine Skills gefunden.
|
||||
settings.agentBehavior.skills.content.empty=Keine Skill-Inhalte verfügbar.
|
||||
settings.agentBehavior.skills.load.timeout=Das Laden der Skills ist abgelaufen. Vorhandene Skills wurden beibehalten; entfernen Sie langsame oder nicht erreichbare URLs und aktualisieren Sie.
|
||||
settings.agentBehavior.skills.reload.deferred=Skill-Quelle gespeichert. Laden Sie Core neu, nachdem aktive Sitzungen beendet sind, um neue Skills anzuwenden.
|
||||
settings.agentBehavior.skills.reload.blocked=Skill-Einstellungen gespeichert, aber es sind aktive Sitzungen vorhanden. Laden Sie Core neu, nachdem diese Sitzungen beendet sind, um neue Skills anzuwenden.
|
||||
settings.agentBehavior.skills.saved.notification=Skill-Einstellungen gespeichert
|
||||
settings.agentBehavior.skills.delete.title=Skill löschen
|
||||
settings.agentBehavior.skills.delete.message=Skill {0} löschen? Dadurch wird die Skill-Datei entfernt und kann nicht rückgängig gemacht werden.
|
||||
settings.agentBehavior.skills.delete.failed=Der Skill konnte nicht gelöscht werden.
|
||||
settings.agentBehavior.skills.openInEditor=Im Editor öffnen
|
||||
settings.agentBehavior.skills.openInEditor.pending=Die Skill-Datei wird geöffnet, nachdem Sie die Einstellungen geschlossen haben.
|
||||
settings.agentBehavior.skills.openInEditor.failed=Die Skill-Datei konnte nicht im Editor geöffnet werden.
|
||||
settings.agentBehavior.skills.sources.empty=Keine Skill-Quellen konfiguriert.
|
||||
settings.agentBehavior.skills.sources.title=Zusätzliche Skill-Quellen
|
||||
settings.agentBehavior.skills.sources.add=Hinzufügen
|
||||
settings.agentBehavior.skills.sources.addPath=Pfad hinzufügen
|
||||
settings.agentBehavior.skills.sources.addPath.title=Skill-Pfad hinzufügen
|
||||
settings.agentBehavior.skills.sources.addPath.prompt=Wählen Sie einen Ordner mit Kilo-Skills aus.
|
||||
settings.agentBehavior.skills.sources.addUrl=URL hinzufügen
|
||||
settings.agentBehavior.skills.sources.addUrl.title=Skill-Quellen-URL hinzufügen
|
||||
settings.agentBehavior.skills.sources.addUrl.prompt=Geben Sie eine Skill-Quellen-URL ein.
|
||||
settings.agentBehavior.skills.sources.editPath.title=Skill-Pfad bearbeiten
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Skill-URL bearbeiten
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.count.one={0} Nachricht zurückgesetzt
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
common.delete=Eliminar
|
||||
common.open=Abrir
|
||||
common.save=Guardar
|
||||
session.action.cancel=Cancelar
|
||||
session.connection.connecting=Cargando…
|
||||
session.connection.error.app=Error de conexión
|
||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
||||
|
||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||
settings.agentBehavior.mcp.addHint=Para agregar un servidor MCP, pídele al agente que lo haga.
|
||||
settings.agentBehavior.skills.displayName=Habilidades
|
||||
settings.agentBehavior.skills.search=Filtrar habilidades
|
||||
settings.agentBehavior.skills.empty=No se encontraron habilidades.
|
||||
settings.agentBehavior.skills.content.empty=No hay contenido de habilidad disponible.
|
||||
settings.agentBehavior.skills.load.timeout=Se agotó el tiempo de carga de habilidades. Se conservaron las habilidades existentes; elimina las URL lentas o inaccesibles y actualiza.
|
||||
settings.agentBehavior.skills.reload.deferred=Fuente de habilidades guardada. Recarga Core cuando terminen las sesiones activas para aplicar nuevas habilidades.
|
||||
settings.agentBehavior.skills.reload.blocked=Configuración de habilidades guardada, pero hay sesiones activas. Recarga Core cuando esas sesiones terminen para aplicar las nuevas habilidades.
|
||||
settings.agentBehavior.skills.saved.notification=Configuración de habilidades guardada
|
||||
settings.agentBehavior.skills.delete.title=Eliminar habilidad
|
||||
settings.agentBehavior.skills.delete.message=¿Eliminar la habilidad {0}? Esto elimina el archivo de la habilidad y no se puede deshacer.
|
||||
settings.agentBehavior.skills.delete.failed=No se pudo eliminar la habilidad.
|
||||
settings.agentBehavior.skills.openInEditor=Abrir en el editor
|
||||
settings.agentBehavior.skills.openInEditor.pending=El archivo de la habilidad se abrirá después de cerrar Configuración.
|
||||
settings.agentBehavior.skills.openInEditor.failed=No se pudo abrir el archivo de la habilidad en el editor.
|
||||
settings.agentBehavior.skills.sources.empty=No hay fuentes de habilidades configuradas.
|
||||
settings.agentBehavior.skills.sources.title=Fuentes de habilidades adicionales
|
||||
settings.agentBehavior.skills.sources.add=Agregar
|
||||
settings.agentBehavior.skills.sources.addPath=Agregar ruta
|
||||
settings.agentBehavior.skills.sources.addPath.title=Agregar ruta de habilidades
|
||||
settings.agentBehavior.skills.sources.addPath.prompt=Elige una carpeta que contenga habilidades de Kilo.
|
||||
settings.agentBehavior.skills.sources.addUrl=Agregar URL
|
||||
settings.agentBehavior.skills.sources.addUrl.title=Agregar URL de fuente de habilidades
|
||||
settings.agentBehavior.skills.sources.addUrl.prompt=Introduce una URL de fuente de habilidades.
|
||||
settings.agentBehavior.skills.sources.editPath.title=Editar ruta de habilidades
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Editar URL de habilidades
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.count.one={0} mensaje revertido
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
common.delete=Supprimer
|
||||
common.open=Ouvrir
|
||||
common.save=Enregistrer
|
||||
session.action.cancel=Annuler
|
||||
session.connection.connecting=Chargement…
|
||||
session.connection.error.app=Échec de la connexion
|
||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
||||
|
||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||
settings.agentBehavior.mcp.addHint=Pour ajouter un serveur MCP, demandez à l’agent de le faire.
|
||||
settings.agentBehavior.skills.displayName=Compétences
|
||||
settings.agentBehavior.skills.search=Filtrer les compétences
|
||||
settings.agentBehavior.skills.empty=Aucune compétence trouvée.
|
||||
settings.agentBehavior.skills.content.empty=Aucun contenu de compétence disponible.
|
||||
settings.agentBehavior.skills.load.timeout=Le chargement des compétences a expiré. Les compétences existantes ont été conservées ; supprimez les URL lentes ou inaccessibles puis actualisez.
|
||||
settings.agentBehavior.skills.reload.deferred=Source de compétences enregistrée. Rechargez Core après la fin des sessions actives pour appliquer les nouvelles compétences.
|
||||
settings.agentBehavior.skills.reload.blocked=Paramètres des compétences enregistrés, mais des sessions sont actives. Rechargez Core après la fin de ces sessions pour appliquer les nouvelles compétences.
|
||||
settings.agentBehavior.skills.saved.notification=Paramètres des compétences enregistrés
|
||||
settings.agentBehavior.skills.delete.title=Supprimer la compétence
|
||||
settings.agentBehavior.skills.delete.message=Supprimer la compétence {0} ? Cela supprime le fichier de compétence et ne peut pas être annulé.
|
||||
settings.agentBehavior.skills.delete.failed=Impossible de supprimer la compétence.
|
||||
settings.agentBehavior.skills.openInEditor=Ouvrir dans l’éditeur
|
||||
settings.agentBehavior.skills.openInEditor.pending=Le fichier de compétence s’ouvrira après la fermeture des paramètres.
|
||||
settings.agentBehavior.skills.openInEditor.failed=Impossible d’ouvrir le fichier de compétence dans l’éditeur.
|
||||
settings.agentBehavior.skills.sources.empty=Aucune source de compétences configurée.
|
||||
settings.agentBehavior.skills.sources.title=Sources de compétences supplémentaires
|
||||
settings.agentBehavior.skills.sources.add=Ajouter
|
||||
settings.agentBehavior.skills.sources.addPath=Ajouter un chemin
|
||||
settings.agentBehavior.skills.sources.addPath.title=Ajouter un chemin de compétences
|
||||
settings.agentBehavior.skills.sources.addPath.prompt=Choisissez un dossier contenant des compétences Kilo.
|
||||
settings.agentBehavior.skills.sources.addUrl=Ajouter une URL
|
||||
settings.agentBehavior.skills.sources.addUrl.title=Ajouter une URL de source de compétences
|
||||
settings.agentBehavior.skills.sources.addUrl.prompt=Saisissez une URL de source de compétences.
|
||||
settings.agentBehavior.skills.sources.editPath.title=Modifier le chemin des compétences
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Modifier l’URL des compétences
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.count.one={0} message annulé
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
common.delete=削除
|
||||
common.open=開く
|
||||
common.save=保存
|
||||
session.action.cancel=キャンセル
|
||||
session.connection.connecting=読み込み中…
|
||||
session.connection.error.app=接続に失敗しました
|
||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
||||
|
||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||
settings.agentBehavior.mcp.addHint=MCPサーバーを追加するには、エージェントに依頼してください。
|
||||
settings.agentBehavior.skills.displayName=スキル
|
||||
settings.agentBehavior.skills.search=スキルを絞り込み
|
||||
settings.agentBehavior.skills.empty=スキルが見つかりません。
|
||||
settings.agentBehavior.skills.content.empty=利用可能なスキル内容がありません。
|
||||
settings.agentBehavior.skills.load.timeout=スキルの読み込みがタイムアウトしました。既存のスキルは保持されました。遅い、または到達不能な URL を削除して更新してください。
|
||||
settings.agentBehavior.skills.reload.deferred=スキルソースを保存しました。新しいスキルを適用するには、アクティブなセッションが終了した後に Core を再読み込みしてください。
|
||||
settings.agentBehavior.skills.reload.blocked=スキル設定を保存しましたが、アクティブなセッションがあります。新しいスキルを適用するには、それらのセッションが終了した後に Core を再読み込みしてください。
|
||||
settings.agentBehavior.skills.saved.notification=スキル設定を保存しました
|
||||
settings.agentBehavior.skills.delete.title=スキルを削除
|
||||
settings.agentBehavior.skills.delete.message=スキル {0} を削除しますか?スキルファイルが削除され、この操作は元に戻せません。
|
||||
settings.agentBehavior.skills.delete.failed=スキルを削除できませんでした。
|
||||
settings.agentBehavior.skills.openInEditor=エディターで開く
|
||||
settings.agentBehavior.skills.openInEditor.pending=設定を閉じるとスキルファイルが開きます。
|
||||
settings.agentBehavior.skills.openInEditor.failed=エディターでスキルファイルを開けませんでした。
|
||||
settings.agentBehavior.skills.sources.empty=スキルソースが設定されていません。
|
||||
settings.agentBehavior.skills.sources.title=追加のスキルソース
|
||||
settings.agentBehavior.skills.sources.add=追加
|
||||
settings.agentBehavior.skills.sources.addPath=パスを追加
|
||||
settings.agentBehavior.skills.sources.addPath.title=スキルパスを追加
|
||||
settings.agentBehavior.skills.sources.addPath.prompt=Kilo スキルを含むフォルダーを選択してください。
|
||||
settings.agentBehavior.skills.sources.addUrl=URL を追加
|
||||
settings.agentBehavior.skills.sources.addUrl.title=スキルソース URL を追加
|
||||
settings.agentBehavior.skills.sources.addUrl.prompt=スキルソース URL を入力してください。
|
||||
settings.agentBehavior.skills.sources.editPath.title=スキルパスを編集
|
||||
settings.agentBehavior.skills.sources.editUrl.title=スキル URL を編集
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.count.one={0} 件のメッセージをロールバックしました
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
common.delete=삭제
|
||||
common.open=열기
|
||||
common.save=저장
|
||||
session.action.cancel=취소
|
||||
session.connection.connecting=로딩 중…
|
||||
session.connection.error.app=연결 실패
|
||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
||||
|
||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||
settings.agentBehavior.mcp.addHint=MCP 서버를 추가하려면 에이전트에게 요청하세요.
|
||||
settings.agentBehavior.skills.displayName=스킬
|
||||
settings.agentBehavior.skills.search=스킬 필터링
|
||||
settings.agentBehavior.skills.empty=스킬을 찾을 수 없습니다.
|
||||
settings.agentBehavior.skills.content.empty=사용 가능한 스킬 내용이 없습니다.
|
||||
settings.agentBehavior.skills.load.timeout=스킬 로드 시간이 초과되었습니다. 기존 스킬은 유지되었습니다. 느리거나 연결할 수 없는 URL을 제거한 뒤 새로 고치세요.
|
||||
settings.agentBehavior.skills.reload.deferred=스킬 소스가 저장되었습니다. 새 스킬을 적용하려면 활성 세션이 끝난 뒤 Core를 다시 로드하세요.
|
||||
settings.agentBehavior.skills.reload.blocked=스킬 설정이 저장되었지만 활성 세션이 있습니다. 새 스킬을 적용하려면 해당 세션이 끝난 뒤 Core를 다시 로드하세요.
|
||||
settings.agentBehavior.skills.saved.notification=스킬 설정이 저장되었습니다
|
||||
settings.agentBehavior.skills.delete.title=스킬 삭제
|
||||
settings.agentBehavior.skills.delete.message=스킬 {0}을 삭제할까요? 스킬 파일이 제거되며 되돌릴 수 없습니다.
|
||||
settings.agentBehavior.skills.delete.failed=스킬을 삭제할 수 없습니다.
|
||||
settings.agentBehavior.skills.openInEditor=에디터에서 열기
|
||||
settings.agentBehavior.skills.openInEditor.pending=설정을 닫으면 스킬 파일이 열립니다.
|
||||
settings.agentBehavior.skills.openInEditor.failed=에디터에서 스킬 파일을 열 수 없습니다.
|
||||
settings.agentBehavior.skills.sources.empty=구성된 스킬 소스가 없습니다.
|
||||
settings.agentBehavior.skills.sources.title=추가 스킬 소스
|
||||
settings.agentBehavior.skills.sources.add=추가
|
||||
settings.agentBehavior.skills.sources.addPath=경로 추가
|
||||
settings.agentBehavior.skills.sources.addPath.title=스킬 경로 추가
|
||||
settings.agentBehavior.skills.sources.addPath.prompt=Kilo 스킬이 포함된 폴더를 선택하세요.
|
||||
settings.agentBehavior.skills.sources.addUrl=URL 추가
|
||||
settings.agentBehavior.skills.sources.addUrl.title=스킬 소스 URL 추가
|
||||
settings.agentBehavior.skills.sources.addUrl.prompt=스킬 소스 URL을 입력하세요.
|
||||
settings.agentBehavior.skills.sources.editPath.title=스킬 경로 편집
|
||||
settings.agentBehavior.skills.sources.editUrl.title=스킬 URL 편집
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.count.one={0}개 메시지가 롤백됨
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
common.delete=Verwijderen
|
||||
common.open=Openen
|
||||
common.save=Opslaan
|
||||
session.action.cancel=Annuleren
|
||||
session.connection.connecting=Laden…
|
||||
session.connection.error.app=Verbinding mislukt
|
||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
||||
|
||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||
settings.agentBehavior.mcp.addHint=Vraag de agent om een MCP-server toe te voegen.
|
||||
settings.agentBehavior.skills.displayName=Vaardigheden
|
||||
settings.agentBehavior.skills.search=Vaardigheden filteren
|
||||
settings.agentBehavior.skills.empty=Geen vaardigheden gevonden.
|
||||
settings.agentBehavior.skills.content.empty=Geen vaardigheidsinhoud beschikbaar.
|
||||
settings.agentBehavior.skills.load.timeout=Het laden van vaardigheden is verlopen. Bestaande vaardigheden zijn behouden; verwijder trage of onbereikbare URL’s en vernieuw.
|
||||
settings.agentBehavior.skills.reload.deferred=Vaardigheidsbron opgeslagen. Laad Core opnieuw nadat actieve sessies zijn voltooid om nieuwe vaardigheden toe te passen.
|
||||
settings.agentBehavior.skills.reload.blocked=Vaardigheidsinstellingen opgeslagen, maar er zijn actieve sessies. Laad Core opnieuw nadat die sessies zijn voltooid om de nieuwe vaardigheden toe te passen.
|
||||
settings.agentBehavior.skills.saved.notification=Vaardigheidsinstellingen opgeslagen
|
||||
settings.agentBehavior.skills.delete.title=Vaardigheid verwijderen
|
||||
settings.agentBehavior.skills.delete.message=Vaardigheid {0} verwijderen? Dit verwijdert het vaardigheidsbestand en kan niet ongedaan worden gemaakt.
|
||||
settings.agentBehavior.skills.delete.failed=Kon de vaardigheid niet verwijderen.
|
||||
settings.agentBehavior.skills.openInEditor=Openen in editor
|
||||
settings.agentBehavior.skills.openInEditor.pending=Het vaardigheidsbestand wordt geopend nadat u Instellingen sluit.
|
||||
settings.agentBehavior.skills.openInEditor.failed=Kon het vaardigheidsbestand niet openen in de editor.
|
||||
settings.agentBehavior.skills.sources.empty=Geen vaardigheidsbronnen geconfigureerd.
|
||||
settings.agentBehavior.skills.sources.title=Extra vaardigheidsbronnen
|
||||
settings.agentBehavior.skills.sources.add=Toevoegen
|
||||
settings.agentBehavior.skills.sources.addPath=Pad toevoegen
|
||||
settings.agentBehavior.skills.sources.addPath.title=Vaardigheidspad toevoegen
|
||||
settings.agentBehavior.skills.sources.addPath.prompt=Kies een map met Kilo-vaardigheden.
|
||||
settings.agentBehavior.skills.sources.addUrl=URL toevoegen
|
||||
settings.agentBehavior.skills.sources.addUrl.title=URL van vaardigheidsbron toevoegen
|
||||
settings.agentBehavior.skills.sources.addUrl.prompt=Voer een URL van een vaardigheidsbron in.
|
||||
settings.agentBehavior.skills.sources.editPath.title=Vaardigheidspad bewerken
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Vaardigheids-URL bewerken
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.count.one={0} bericht teruggedraaid
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
common.delete=Slett
|
||||
common.open=Åpne
|
||||
common.save=Lagre
|
||||
session.action.cancel=Avbryt
|
||||
session.connection.connecting=Laster…
|
||||
session.connection.error.app=Tilkoblingsfeil
|
||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
||||
|
||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||
settings.agentBehavior.mcp.addHint=Be agenten om å legge til en MCP-server.
|
||||
settings.agentBehavior.skills.displayName=Ferdigheter
|
||||
settings.agentBehavior.skills.search=Filtrer ferdigheter
|
||||
settings.agentBehavior.skills.empty=Ingen ferdigheter funnet.
|
||||
settings.agentBehavior.skills.content.empty=Ingen ferdighetsinnhold tilgjengelig.
|
||||
settings.agentBehavior.skills.load.timeout=Innlasting av ferdigheter tidsavbrutt. Eksisterende ferdigheter ble beholdt; fjern trege eller utilgjengelige URL-er og oppdater.
|
||||
settings.agentBehavior.skills.reload.deferred=Ferdighetskilden ble lagret. Last Core på nytt etter at aktive økter er ferdige for å bruke nye ferdigheter.
|
||||
settings.agentBehavior.skills.reload.blocked=Ferdighetsinnstillinger ble lagret, men det finnes aktive økter. Last Core på nytt etter at disse øktene er ferdige for å bruke de nye ferdighetene.
|
||||
settings.agentBehavior.skills.saved.notification=Ferdighetsinnstillinger lagret
|
||||
settings.agentBehavior.skills.delete.title=Slett ferdighet
|
||||
settings.agentBehavior.skills.delete.message=Slette ferdigheten {0}? Dette fjerner ferdighetsfilen og kan ikke angres.
|
||||
settings.agentBehavior.skills.delete.failed=Kunne ikke slette ferdigheten.
|
||||
settings.agentBehavior.skills.openInEditor=Åpne i editor
|
||||
settings.agentBehavior.skills.openInEditor.pending=Ferdighetsfilen åpnes etter at du lukker Innstillinger.
|
||||
settings.agentBehavior.skills.openInEditor.failed=Kunne ikke åpne ferdighetsfilen i editoren.
|
||||
settings.agentBehavior.skills.sources.empty=Ingen ferdighetskilder konfigurert.
|
||||
settings.agentBehavior.skills.sources.title=Flere ferdighetskilder
|
||||
settings.agentBehavior.skills.sources.add=Legg til
|
||||
settings.agentBehavior.skills.sources.addPath=Legg til sti
|
||||
settings.agentBehavior.skills.sources.addPath.title=Legg til ferdighetssti
|
||||
settings.agentBehavior.skills.sources.addPath.prompt=Velg en mappe som inneholder Kilo-ferdigheter.
|
||||
settings.agentBehavior.skills.sources.addUrl=Legg til URL
|
||||
settings.agentBehavior.skills.sources.addUrl.title=Legg til URL for ferdighetskilde
|
||||
settings.agentBehavior.skills.sources.addUrl.prompt=Skriv inn en URL for ferdighetskilde.
|
||||
settings.agentBehavior.skills.sources.editPath.title=Rediger ferdighetssti
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Rediger ferdighets-URL
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.count.one={0} melding rullet tilbake
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
common.delete=Usuń
|
||||
common.open=Otwórz
|
||||
common.save=Zapisz
|
||||
session.action.cancel=Anuluj
|
||||
session.connection.connecting=Ładowanie…
|
||||
session.connection.error.app=Błąd połączenia
|
||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
||||
|
||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||
settings.agentBehavior.mcp.addHint=Aby dodać serwer MCP, poproś agenta, aby to zrobił.
|
||||
settings.agentBehavior.skills.displayName=Umiejętności
|
||||
settings.agentBehavior.skills.search=Filtruj umiejętności
|
||||
settings.agentBehavior.skills.empty=Nie znaleziono umiejętności.
|
||||
settings.agentBehavior.skills.content.empty=Brak dostępnej treści umiejętności.
|
||||
settings.agentBehavior.skills.load.timeout=Przekroczono czas ładowania umiejętności. Istniejące umiejętności zostały zachowane; usuń wolne lub niedostępne adresy URL i odśwież.
|
||||
settings.agentBehavior.skills.reload.deferred=Źródło umiejętności zapisane. Przeładuj Core po zakończeniu aktywnych sesji, aby zastosować nowe umiejętności.
|
||||
settings.agentBehavior.skills.reload.blocked=Ustawienia umiejętności zapisane, ale są aktywne sesje. Przeładuj Core po zakończeniu tych sesji, aby zastosować nowe umiejętności.
|
||||
settings.agentBehavior.skills.saved.notification=Ustawienia umiejętności zapisane
|
||||
settings.agentBehavior.skills.delete.title=Usuń umiejętność
|
||||
settings.agentBehavior.skills.delete.message=Usunąć umiejętność {0}? Spowoduje to usunięcie pliku umiejętności i nie można tego cofnąć.
|
||||
settings.agentBehavior.skills.delete.failed=Nie można usunąć umiejętności.
|
||||
settings.agentBehavior.skills.openInEditor=Otwórz w edytorze
|
||||
settings.agentBehavior.skills.openInEditor.pending=Plik umiejętności otworzy się po zamknięciu Ustawień.
|
||||
settings.agentBehavior.skills.openInEditor.failed=Nie można otworzyć pliku umiejętności w edytorze.
|
||||
settings.agentBehavior.skills.sources.empty=Nie skonfigurowano źródeł umiejętności.
|
||||
settings.agentBehavior.skills.sources.title=Dodatkowe źródła umiejętności
|
||||
settings.agentBehavior.skills.sources.add=Dodaj
|
||||
settings.agentBehavior.skills.sources.addPath=Dodaj ścieżkę
|
||||
settings.agentBehavior.skills.sources.addPath.title=Dodaj ścieżkę umiejętności
|
||||
settings.agentBehavior.skills.sources.addPath.prompt=Wybierz folder zawierający umiejętności Kilo.
|
||||
settings.agentBehavior.skills.sources.addUrl=Dodaj URL
|
||||
settings.agentBehavior.skills.sources.addUrl.title=Dodaj URL źródła umiejętności
|
||||
settings.agentBehavior.skills.sources.addUrl.prompt=Wpisz URL źródła umiejętności.
|
||||
settings.agentBehavior.skills.sources.editPath.title=Edytuj ścieżkę umiejętności
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Edytuj URL umiejętności
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.count.one=Cofnięto {0} wiadomość
|
||||
|
||||
+27
@@ -1,4 +1,6 @@
|
||||
common.delete=Excluir
|
||||
common.open=Abrir
|
||||
common.save=Salvar
|
||||
session.action.cancel=Cancelar
|
||||
session.connection.connecting=Carregando…
|
||||
session.connection.error.app=Falha na conexão
|
||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
||||
|
||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||
settings.agentBehavior.mcp.addHint=Para adicionar um servidor MCP, peça ao agente para fazer isso.
|
||||
settings.agentBehavior.skills.displayName=Habilidades
|
||||
settings.agentBehavior.skills.search=Filtrar habilidades
|
||||
settings.agentBehavior.skills.empty=Nenhuma habilidade encontrada.
|
||||
settings.agentBehavior.skills.content.empty=Nenhum conteúdo de habilidade disponível.
|
||||
settings.agentBehavior.skills.load.timeout=O carregamento de habilidades atingiu o tempo limite. As habilidades existentes foram mantidas; remova URLs lentas ou inacessíveis e atualize.
|
||||
settings.agentBehavior.skills.reload.deferred=Fonte de habilidades salva. Recarregue o Core depois que as sessões ativas terminarem para aplicar novas habilidades.
|
||||
settings.agentBehavior.skills.reload.blocked=Configurações de habilidades salvas, mas há sessões ativas. Recarregue o Core depois que essas sessões terminarem para aplicar as novas habilidades.
|
||||
settings.agentBehavior.skills.saved.notification=Configurações de habilidades salvas
|
||||
settings.agentBehavior.skills.delete.title=Excluir habilidade
|
||||
settings.agentBehavior.skills.delete.message=Excluir a habilidade {0}? Isso remove o arquivo da habilidade e não pode ser desfeito.
|
||||
settings.agentBehavior.skills.delete.failed=Não foi possível excluir a habilidade.
|
||||
settings.agentBehavior.skills.openInEditor=Abrir no editor
|
||||
settings.agentBehavior.skills.openInEditor.pending=O arquivo da habilidade será aberto depois que você fechar as Configurações.
|
||||
settings.agentBehavior.skills.openInEditor.failed=Não foi possível abrir o arquivo da habilidade no editor.
|
||||
settings.agentBehavior.skills.sources.empty=Nenhuma fonte de habilidades configurada.
|
||||
settings.agentBehavior.skills.sources.title=Fontes de habilidades adicionais
|
||||
settings.agentBehavior.skills.sources.add=Adicionar
|
||||
settings.agentBehavior.skills.sources.addPath=Adicionar caminho
|
||||
settings.agentBehavior.skills.sources.addPath.title=Adicionar caminho de habilidades
|
||||
settings.agentBehavior.skills.sources.addPath.prompt=Escolha uma pasta contendo habilidades do Kilo.
|
||||
settings.agentBehavior.skills.sources.addUrl=Adicionar URL
|
||||
settings.agentBehavior.skills.sources.addUrl.title=Adicionar URL de fonte de habilidades
|
||||
settings.agentBehavior.skills.sources.addUrl.prompt=Insira uma URL de fonte de habilidades.
|
||||
settings.agentBehavior.skills.sources.editPath.title=Editar caminho de habilidades
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Editar URL de habilidades
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.count.one={0} mensagem revertida
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
common.delete=Удалить
|
||||
common.open=Открыть
|
||||
common.save=Сохранить
|
||||
session.action.cancel=Отмена
|
||||
session.connection.connecting=Загрузка…
|
||||
session.connection.error.app=Ошибка подключения
|
||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
||||
|
||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||
settings.agentBehavior.mcp.addHint=Чтобы добавить MCP-сервер, попросите агента сделать это.
|
||||
settings.agentBehavior.skills.displayName=Навыки
|
||||
settings.agentBehavior.skills.search=Фильтр навыков
|
||||
settings.agentBehavior.skills.empty=Навыки не найдены.
|
||||
settings.agentBehavior.skills.content.empty=Нет доступного содержимого навыка.
|
||||
settings.agentBehavior.skills.load.timeout=Время загрузки навыков истекло. Существующие навыки сохранены; удалите медленные или недоступные URL и обновите.
|
||||
settings.agentBehavior.skills.reload.deferred=Источник навыков сохранён. Перезагрузите Core после завершения активных сессий, чтобы применить новые навыки.
|
||||
settings.agentBehavior.skills.reload.blocked=Настройки навыков сохранены, но есть активные сессии. Перезагрузите Core после завершения этих сессий, чтобы применить новые навыки.
|
||||
settings.agentBehavior.skills.saved.notification=Настройки навыков сохранены
|
||||
settings.agentBehavior.skills.delete.title=Удалить навык
|
||||
settings.agentBehavior.skills.delete.message=Удалить навык {0}? Это удалит файл навыка, и действие нельзя будет отменить.
|
||||
settings.agentBehavior.skills.delete.failed=Не удалось удалить навык.
|
||||
settings.agentBehavior.skills.openInEditor=Открыть в редакторе
|
||||
settings.agentBehavior.skills.openInEditor.pending=Файл навыка откроется после закрытия настроек.
|
||||
settings.agentBehavior.skills.openInEditor.failed=Не удалось открыть файл навыка в редакторе.
|
||||
settings.agentBehavior.skills.sources.empty=Источники навыков не настроены.
|
||||
settings.agentBehavior.skills.sources.title=Дополнительные источники навыков
|
||||
settings.agentBehavior.skills.sources.add=Добавить
|
||||
settings.agentBehavior.skills.sources.addPath=Добавить путь
|
||||
settings.agentBehavior.skills.sources.addPath.title=Добавить путь к навыкам
|
||||
settings.agentBehavior.skills.sources.addPath.prompt=Выберите папку, содержащую навыки Kilo.
|
||||
settings.agentBehavior.skills.sources.addUrl=Добавить URL
|
||||
settings.agentBehavior.skills.sources.addUrl.title=Добавить URL источника навыков
|
||||
settings.agentBehavior.skills.sources.addUrl.prompt=Введите URL источника навыков.
|
||||
settings.agentBehavior.skills.sources.editPath.title=Изменить путь к навыкам
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Изменить URL навыков
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.count.one=Отменено сообщений: {0}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
common.delete=ลบ
|
||||
common.open=เปิด
|
||||
common.save=บันทึก
|
||||
session.action.cancel=ยกเลิก
|
||||
session.connection.connecting=กำลังโหลด…
|
||||
session.connection.error.app=การเชื่อมต่อล้มเหลว
|
||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
||||
|
||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||
settings.agentBehavior.mcp.addHint=หากต้องการเพิ่มเซิร์ฟเวอร์ MCP ให้ขอให้เอเจนต์เพิ่มให้
|
||||
settings.agentBehavior.skills.displayName=ทักษะ
|
||||
settings.agentBehavior.skills.search=กรองทักษะ
|
||||
settings.agentBehavior.skills.empty=ไม่พบทักษะ
|
||||
settings.agentBehavior.skills.content.empty=ไม่มีเนื้อหาทักษะที่พร้อมใช้งาน
|
||||
settings.agentBehavior.skills.load.timeout=การโหลดทักษะหมดเวลา ระบบเก็บทักษะเดิมไว้แล้ว โปรดลบ URL ที่ช้าหรือเข้าถึงไม่ได้แล้วรีเฟรช
|
||||
settings.agentBehavior.skills.reload.deferred=บันทึกแหล่งที่มาทักษะแล้ว โหลด Core ใหม่หลังจากเซสชันที่ใช้งานอยู่สิ้นสุดเพื่อใช้ทักษะใหม่
|
||||
settings.agentBehavior.skills.reload.blocked=บันทึกการตั้งค่าทักษะแล้ว แต่ยังมีเซสชันที่ใช้งานอยู่ โหลด Core ใหม่หลังจากเซสชันเหล่านั้นสิ้นสุดเพื่อใช้ทักษะใหม่
|
||||
settings.agentBehavior.skills.saved.notification=บันทึกการตั้งค่าทักษะแล้ว
|
||||
settings.agentBehavior.skills.delete.title=ลบทักษะ
|
||||
settings.agentBehavior.skills.delete.message=ลบทักษะ {0} หรือไม่ การดำเนินการนี้จะลบไฟล์ทักษะและไม่สามารถย้อนกลับได้
|
||||
settings.agentBehavior.skills.delete.failed=ไม่สามารถลบทักษะได้
|
||||
settings.agentBehavior.skills.openInEditor=เปิดในตัวแก้ไข
|
||||
settings.agentBehavior.skills.openInEditor.pending=ไฟล์ทักษะจะเปิดหลังจากคุณปิดการตั้งค่า
|
||||
settings.agentBehavior.skills.openInEditor.failed=ไม่สามารถเปิดไฟล์ทักษะในตัวแก้ไขได้
|
||||
settings.agentBehavior.skills.sources.empty=ไม่ได้กำหนดค่าแหล่งที่มาทักษะ
|
||||
settings.agentBehavior.skills.sources.title=แหล่งที่มาทักษะเพิ่มเติม
|
||||
settings.agentBehavior.skills.sources.add=เพิ่ม
|
||||
settings.agentBehavior.skills.sources.addPath=เพิ่มเส้นทาง
|
||||
settings.agentBehavior.skills.sources.addPath.title=เพิ่มเส้นทางทักษะ
|
||||
settings.agentBehavior.skills.sources.addPath.prompt=เลือกโฟลเดอร์ที่มีทักษะ Kilo
|
||||
settings.agentBehavior.skills.sources.addUrl=เพิ่ม URL
|
||||
settings.agentBehavior.skills.sources.addUrl.title=เพิ่ม URL แหล่งที่มาทักษะ
|
||||
settings.agentBehavior.skills.sources.addUrl.prompt=ป้อน URL แหล่งที่มาทักษะ
|
||||
settings.agentBehavior.skills.sources.editPath.title=แก้ไขเส้นทางทักษะ
|
||||
settings.agentBehavior.skills.sources.editUrl.title=แก้ไข URL ทักษะ
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.count.one=ย้อนกลับข้อความ {0} รายการแล้ว
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
common.delete=Sil
|
||||
common.open=Aç
|
||||
common.save=Kaydet
|
||||
session.action.cancel=İptal
|
||||
session.connection.connecting=Yükleniyor…
|
||||
session.connection.error.app=Bağlantı hatası
|
||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
||||
|
||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||
settings.agentBehavior.mcp.addHint=MCP sunucusu eklemek için ajandan bunu yapmasını isteyin.
|
||||
settings.agentBehavior.skills.displayName=Beceriler
|
||||
settings.agentBehavior.skills.search=Becerileri filtrele
|
||||
settings.agentBehavior.skills.empty=Beceri bulunamadı.
|
||||
settings.agentBehavior.skills.content.empty=Kullanılabilir beceri içeriği yok.
|
||||
settings.agentBehavior.skills.load.timeout=Beceriler yüklenirken zaman aşımına uğradı. Mevcut beceriler korundu; yavaş veya ulaşılamayan URL'leri kaldırıp yenileyin.
|
||||
settings.agentBehavior.skills.reload.deferred=Beceri kaynağı kaydedildi. Yeni becerileri uygulamak için etkin oturumlar bittikten sonra Core'u yeniden yükleyin.
|
||||
settings.agentBehavior.skills.reload.blocked=Beceri ayarları kaydedildi, ancak etkin oturumlar var. Yeni becerileri uygulamak için bu oturumlar bittikten sonra Core'u yeniden yükleyin.
|
||||
settings.agentBehavior.skills.saved.notification=Beceri ayarları kaydedildi
|
||||
settings.agentBehavior.skills.delete.title=Beceriyi Sil
|
||||
settings.agentBehavior.skills.delete.message={0} becerisi silinsin mi? Bu işlem beceri dosyasını kaldırır ve geri alınamaz.
|
||||
settings.agentBehavior.skills.delete.failed=Beceri silinemedi.
|
||||
settings.agentBehavior.skills.openInEditor=Düzenleyicide Aç
|
||||
settings.agentBehavior.skills.openInEditor.pending=Beceri dosyası Ayarlar kapatıldıktan sonra açılacak.
|
||||
settings.agentBehavior.skills.openInEditor.failed=Beceri dosyası düzenleyicide açılamadı.
|
||||
settings.agentBehavior.skills.sources.empty=Yapılandırılmış beceri kaynağı yok.
|
||||
settings.agentBehavior.skills.sources.title=Ek Beceri Kaynakları
|
||||
settings.agentBehavior.skills.sources.add=Ekle
|
||||
settings.agentBehavior.skills.sources.addPath=Yol ekle
|
||||
settings.agentBehavior.skills.sources.addPath.title=Beceri Yolu Ekle
|
||||
settings.agentBehavior.skills.sources.addPath.prompt=Kilo becerilerini içeren bir klasör seçin.
|
||||
settings.agentBehavior.skills.sources.addUrl=URL ekle
|
||||
settings.agentBehavior.skills.sources.addUrl.title=Beceri Kaynağı URL'si Ekle
|
||||
settings.agentBehavior.skills.sources.addUrl.prompt=Bir beceri kaynağı URL'si girin.
|
||||
settings.agentBehavior.skills.sources.editPath.title=Beceri Yolunu Düzenle
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Beceri URL'sini Düzenle
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.count.one={0} mesaj geri alındı
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
common.delete=Видалити
|
||||
common.open=Відкрити
|
||||
common.save=Зберегти
|
||||
session.action.cancel=Скасувати
|
||||
session.connection.connecting=Завантаження…
|
||||
session.connection.error.app=Помилка з'єднання
|
||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
||||
|
||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||
settings.agentBehavior.mcp.addHint=Щоб додати сервер MCP, попросіть агента зробити це.
|
||||
settings.agentBehavior.skills.displayName=Навички
|
||||
settings.agentBehavior.skills.search=Фільтрувати навички
|
||||
settings.agentBehavior.skills.empty=Навички не знайдено.
|
||||
settings.agentBehavior.skills.content.empty=Немає доступного вмісту навички.
|
||||
settings.agentBehavior.skills.load.timeout=Час завантаження навичок минув. Наявні навички збережено; видаліть повільні або недоступні URL-адреси й оновіть.
|
||||
settings.agentBehavior.skills.reload.deferred=Джерело навичок збережено. Перезавантажте Core після завершення активних сеансів, щоб застосувати нові навички.
|
||||
settings.agentBehavior.skills.reload.blocked=Налаштування навичок збережено, але є активні сеанси. Перезавантажте Core після завершення цих сеансів, щоб застосувати нові навички.
|
||||
settings.agentBehavior.skills.saved.notification=Налаштування навичок збережено
|
||||
settings.agentBehavior.skills.delete.title=Видалити навичку
|
||||
settings.agentBehavior.skills.delete.message=Видалити навичку {0}? Це видалить файл навички, і дію не можна буде скасувати.
|
||||
settings.agentBehavior.skills.delete.failed=Не вдалося видалити навичку.
|
||||
settings.agentBehavior.skills.openInEditor=Відкрити в редакторі
|
||||
settings.agentBehavior.skills.openInEditor.pending=Файл навички відкриється після закриття Налаштувань.
|
||||
settings.agentBehavior.skills.openInEditor.failed=Не вдалося відкрити файл навички в редакторі.
|
||||
settings.agentBehavior.skills.sources.empty=Джерела навичок не налаштовано.
|
||||
settings.agentBehavior.skills.sources.title=Додаткові джерела навичок
|
||||
settings.agentBehavior.skills.sources.add=Додати
|
||||
settings.agentBehavior.skills.sources.addPath=Додати шлях
|
||||
settings.agentBehavior.skills.sources.addPath.title=Додати шлях до навичок
|
||||
settings.agentBehavior.skills.sources.addPath.prompt=Виберіть папку з навичками Kilo.
|
||||
settings.agentBehavior.skills.sources.addUrl=Додати URL
|
||||
settings.agentBehavior.skills.sources.addUrl.title=Додати URL джерела навичок
|
||||
settings.agentBehavior.skills.sources.addUrl.prompt=Введіть URL джерела навичок.
|
||||
settings.agentBehavior.skills.sources.editPath.title=Редагувати шлях до навичок
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Редагувати URL навичок
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.count.one=Відкочено повідомлень: {0}
|
||||
|
||||
+27
@@ -1,4 +1,6 @@
|
||||
common.delete=删除
|
||||
common.open=打开
|
||||
common.save=保存
|
||||
session.action.cancel=取消
|
||||
session.connection.connecting=加载中…
|
||||
session.connection.error.app=连接失败
|
||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
||||
|
||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||
settings.agentBehavior.mcp.addHint=要添加 MCP 服务器,请让代理为你添加。
|
||||
settings.agentBehavior.skills.displayName=技能
|
||||
settings.agentBehavior.skills.search=筛选技能
|
||||
settings.agentBehavior.skills.empty=未找到技能。
|
||||
settings.agentBehavior.skills.content.empty=没有可用的技能内容。
|
||||
settings.agentBehavior.skills.load.timeout=技能加载超时。已保留现有技能;请移除缓慢或无法访问的 URL 后刷新。
|
||||
settings.agentBehavior.skills.reload.deferred=技能源已保存。请在活动会话结束后重新加载 Core,以应用新技能。
|
||||
settings.agentBehavior.skills.reload.blocked=技能设置已保存,但仍有活动会话。请在这些会话结束后重新加载 Core,以应用新技能。
|
||||
settings.agentBehavior.skills.saved.notification=技能设置已保存
|
||||
settings.agentBehavior.skills.delete.title=删除技能
|
||||
settings.agentBehavior.skills.delete.message=要删除技能 {0} 吗?这会移除技能文件,且无法撤销。
|
||||
settings.agentBehavior.skills.delete.failed=无法删除该技能。
|
||||
settings.agentBehavior.skills.openInEditor=在编辑器中打开
|
||||
settings.agentBehavior.skills.openInEditor.pending=关闭设置后将打开技能文件。
|
||||
settings.agentBehavior.skills.openInEditor.failed=无法在编辑器中打开技能文件。
|
||||
settings.agentBehavior.skills.sources.empty=未配置技能源。
|
||||
settings.agentBehavior.skills.sources.title=其他技能源
|
||||
settings.agentBehavior.skills.sources.add=添加
|
||||
settings.agentBehavior.skills.sources.addPath=添加路径
|
||||
settings.agentBehavior.skills.sources.addPath.title=添加技能路径
|
||||
settings.agentBehavior.skills.sources.addPath.prompt=选择一个包含 Kilo 技能的文件夹。
|
||||
settings.agentBehavior.skills.sources.addUrl=添加 URL
|
||||
settings.agentBehavior.skills.sources.addUrl.title=添加技能源 URL
|
||||
settings.agentBehavior.skills.sources.addUrl.prompt=输入技能源 URL。
|
||||
settings.agentBehavior.skills.sources.editPath.title=编辑技能路径
|
||||
settings.agentBehavior.skills.sources.editUrl.title=编辑技能 URL
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.count.one=已回滚 {0} 条消息
|
||||
|
||||
+27
@@ -1,4 +1,6 @@
|
||||
common.delete=刪除
|
||||
common.open=開啟
|
||||
common.save=儲存
|
||||
session.action.cancel=取消
|
||||
session.connection.connecting=載入中…
|
||||
session.connection.error.app=連線失敗
|
||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
||||
|
||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||
settings.agentBehavior.mcp.addHint=若要新增 MCP 伺服器,請請代理為你新增。
|
||||
settings.agentBehavior.skills.displayName=技能
|
||||
settings.agentBehavior.skills.search=篩選技能
|
||||
settings.agentBehavior.skills.empty=找不到技能。
|
||||
settings.agentBehavior.skills.content.empty=沒有可用的技能內容。
|
||||
settings.agentBehavior.skills.load.timeout=技能載入逾時。已保留現有技能;請移除緩慢或無法連線的 URL 後重新整理。
|
||||
settings.agentBehavior.skills.reload.deferred=技能來源已儲存。請在作用中工作階段結束後重新載入 Core,以套用新技能。
|
||||
settings.agentBehavior.skills.reload.blocked=技能設定已儲存,但仍有作用中工作階段。請在這些工作階段結束後重新載入 Core,以套用新技能。
|
||||
settings.agentBehavior.skills.saved.notification=技能設定已儲存
|
||||
settings.agentBehavior.skills.delete.title=刪除技能
|
||||
settings.agentBehavior.skills.delete.message=要刪除技能 {0} 嗎?這會移除技能檔案,且無法復原。
|
||||
settings.agentBehavior.skills.delete.failed=無法刪除該技能。
|
||||
settings.agentBehavior.skills.openInEditor=在編輯器中開啟
|
||||
settings.agentBehavior.skills.openInEditor.pending=關閉設定後將開啟技能檔案。
|
||||
settings.agentBehavior.skills.openInEditor.failed=無法在編輯器中開啟技能檔案。
|
||||
settings.agentBehavior.skills.sources.empty=未設定技能來源。
|
||||
settings.agentBehavior.skills.sources.title=其他技能來源
|
||||
settings.agentBehavior.skills.sources.add=新增
|
||||
settings.agentBehavior.skills.sources.addPath=新增路徑
|
||||
settings.agentBehavior.skills.sources.addPath.title=新增技能路徑
|
||||
settings.agentBehavior.skills.sources.addPath.prompt=選擇包含 Kilo 技能的資料夾。
|
||||
settings.agentBehavior.skills.sources.addUrl=新增 URL
|
||||
settings.agentBehavior.skills.sources.addUrl.title=新增技能來源 URL
|
||||
settings.agentBehavior.skills.sources.addUrl.prompt=輸入技能來源 URL。
|
||||
settings.agentBehavior.skills.sources.editPath.title=編輯技能路徑
|
||||
settings.agentBehavior.skills.sources.editUrl.title=編輯技能 URL
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.count.one=已回復 {0} 則訊息
|
||||
|
||||
+31
@@ -3,6 +3,7 @@ package ai.kilocode.client.app
|
||||
import ai.kilocode.client.testing.FakeAgentBehaviorRpcApi
|
||||
import ai.kilocode.rpc.dto.AgentCreateDto
|
||||
import ai.kilocode.rpc.dto.McpStatusDto
|
||||
import ai.kilocode.rpc.dto.SkillDto
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -10,6 +11,7 @@ import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class KiloAgentBehaviorServiceTest : BasePlatformTestCase() {
|
||||
private lateinit var scope: CoroutineScope
|
||||
@@ -67,6 +69,35 @@ class KiloAgentBehaviorServiceTest : BasePlatformTestCase() {
|
||||
assertTrue(rpc.removals.isEmpty())
|
||||
}
|
||||
|
||||
fun `test loadSkills propagates rpc failure`() = runBlocking {
|
||||
rpc.skillsError = RuntimeException("boom")
|
||||
|
||||
assertFailsWith<RuntimeException> {
|
||||
withContext(Dispatchers.Default) { service.loadSkills("/test") }
|
||||
}
|
||||
}
|
||||
|
||||
fun `test refreshSkills returns previous rows on rpc failure`() = runBlocking {
|
||||
val fallback = listOf(SkillDto("plan", location = "/test/SKILL.md"))
|
||||
rpc.skillsError = RuntimeException("boom")
|
||||
|
||||
val items = withContext(Dispatchers.Default) { service.refreshSkills("/test", fallback) }
|
||||
|
||||
assertEquals(fallback, items)
|
||||
}
|
||||
|
||||
fun `test saveSkills forwards all edits`() = runBlocking {
|
||||
rpc.skills = listOf(SkillDto("plan", location = "/test/plan/SKILL.md"))
|
||||
|
||||
val ok = withContext(Dispatchers.Default) {
|
||||
service.saveSkills("/test", mapOf("/test/plan/SKILL.md" to "# Saved"))
|
||||
}
|
||||
|
||||
assertTrue(ok)
|
||||
assertEquals(listOf(Triple("/test", "/test/plan/SKILL.md", "# Saved")), rpc.skillSaves)
|
||||
assertEquals("# Saved", rpc.skills.single().content)
|
||||
}
|
||||
|
||||
fun `test mcpStatus forwards directory`() = runBlocking {
|
||||
rpc.mcps = listOf(McpStatusDto("filesystem", "connected"))
|
||||
|
||||
|
||||
+2
-1
@@ -18,6 +18,7 @@ class AgentBehaviorConfigurableTest : BasePlatformTestCase() {
|
||||
fun `test child ids match xml registration`() {
|
||||
assertEquals("ai.kilocode.jetbrains.settings.agentBehavior.agents", AgentsConfigurable.ID)
|
||||
assertEquals("ai.kilocode.jetbrains.settings.agentBehavior.mcp", McpConfigurable.ID)
|
||||
assertEquals("ai.kilocode.jetbrains.settings.agentBehavior.skills", SkillsConfigurable.ID)
|
||||
}
|
||||
|
||||
fun `test createComponent contains child links in order`() {
|
||||
@@ -26,7 +27,7 @@ class AgentBehaviorConfigurableTest : BasePlatformTestCase() {
|
||||
edt {
|
||||
val panel = cfg.createComponent()
|
||||
val labels = links(panel as Container).map { it.text }
|
||||
assertEquals(listOf("Agents", "MCP Servers"), labels)
|
||||
assertEquals(listOf("Agents", "MCP Servers", "Skills"), labels)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+615
@@ -0,0 +1,615 @@
|
||||
package ai.kilocode.client.settings.agents
|
||||
|
||||
import ai.kilocode.client.app.KiloAgentBehaviorService
|
||||
import ai.kilocode.client.app.KiloAppService
|
||||
import ai.kilocode.client.app.KiloWorkspaceService
|
||||
import ai.kilocode.client.settings.base.SettingsListItem
|
||||
import ai.kilocode.client.settings.base.settingsListCellBounds
|
||||
import ai.kilocode.client.testing.FakeAgentBehaviorRpcApi
|
||||
import ai.kilocode.client.testing.FakeAppRpcApi
|
||||
import ai.kilocode.client.testing.FakeWorkspaceRpcApi
|
||||
import ai.kilocode.client.testing.fire
|
||||
import ai.kilocode.rpc.dto.ConfigDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStateDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStatusDto
|
||||
import ai.kilocode.rpc.dto.SkillDto
|
||||
import ai.kilocode.rpc.dto.SkillsConfigDto
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.fileTypes.FileTypeManager
|
||||
import com.intellij.openapi.fileTypes.PlainTextFileType
|
||||
import com.intellij.openapi.fileTypes.UnknownFileType
|
||||
import com.intellij.openapi.ui.DialogWrapper
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import com.intellij.openapi.ui.TestDialog
|
||||
import com.intellij.openapi.ui.TestDialogManager
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import com.intellij.testFramework.replaceService
|
||||
import com.intellij.ui.TitledSeparator
|
||||
import com.intellij.ui.SimpleColoredComponent
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.components.JBList
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Container
|
||||
import java.awt.Dimension
|
||||
import java.awt.Point
|
||||
import java.awt.event.InputEvent
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.ScrollPaneConstants
|
||||
import javax.swing.Scrollable
|
||||
import javax.swing.JTextField
|
||||
|
||||
class SkillsSettingsUiTest : BasePlatformTestCase() {
|
||||
private var scope: CoroutineScope? = null
|
||||
private var ui: SkillsSettingsUi? = null
|
||||
private lateinit var app: KiloAppService
|
||||
private lateinit var appRpc: FakeAppRpcApi
|
||||
private lateinit var agentRpc: FakeAgentBehaviorRpcApi
|
||||
private lateinit var workspaceRpc: FakeWorkspaceRpcApi
|
||||
private var shown = 0
|
||||
|
||||
override fun tearDown() {
|
||||
try {
|
||||
TestDialogManager.setTestDialog(TestDialog.DEFAULT)
|
||||
ui?.let { panel -> edt { panel.dispose(); true } }
|
||||
ui = null
|
||||
scope?.cancel()
|
||||
scope = null
|
||||
} finally {
|
||||
super.tearDown()
|
||||
}
|
||||
}
|
||||
|
||||
fun `test loads skills with location note and builtins have no actions`() {
|
||||
val panel = panel()
|
||||
|
||||
flushUntil { rows(panel).size == 3 }
|
||||
|
||||
edt {
|
||||
val rows = rows(panel)
|
||||
val custom = rows.single { it.key == CUSTOM }
|
||||
assertEquals("plan", custom.title)
|
||||
assertEquals(CUSTOM, custom.note)
|
||||
assertEquals("Plan work", custom.description)
|
||||
assertEquals("edit", custom.doubleClick)
|
||||
assertEquals(listOf("open", "edit", "delete"), custom.cells.map { it.id })
|
||||
assertTrue(custom.cells.single { it.id == "open" }.primary)
|
||||
assertFalse(custom.cells.single { it.id == "edit" }.primary)
|
||||
assertEquals("Edit", custom.cells.single { it.id == "edit" }.label)
|
||||
assertTrue(custom.cells.single { it.id == "delete" }.iconOnly)
|
||||
val builtin = rows.single { it.key == "builtin" }
|
||||
assertEquals("thinking", builtin.title)
|
||||
assertNull(builtin.note)
|
||||
assertEquals("edit", builtin.doubleClick)
|
||||
assertEquals(listOf("built-in"), builtin.badges.map { it.text })
|
||||
assertEquals(listOf("edit"), builtin.cells.map { it.id })
|
||||
assertEquals("Open", builtin.cells.single().label)
|
||||
val remote = rows.single { it.key == REMOTE }
|
||||
assertEquals(listOf("edit"), remote.cells.map { it.id })
|
||||
assertEquals("Open", remote.cells.single().label)
|
||||
assertEquals(listOf(DIR), agentRpc.skillCalls)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun `test skills list is vertically scrolled without horizontal scrollbar`() {
|
||||
val panel = panel()
|
||||
flushUntil { rows(panel).size == 3 }
|
||||
|
||||
edt {
|
||||
val pane = scrollFor(panel, skillsList(panel))
|
||||
val view = pane.viewport.view
|
||||
val layout = panel.content.layout as BorderLayout
|
||||
|
||||
assertEquals(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER, pane.horizontalScrollBarPolicy)
|
||||
assertTrue((view as Scrollable).getScrollableTracksViewportWidth())
|
||||
assertFalse(view.getScrollableTracksViewportHeight())
|
||||
assertSame(pane, layout.getLayoutComponent(BorderLayout.CENTER))
|
||||
assertSame(panel.sources, layout.getLayoutComponent(BorderLayout.SOUTH))
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun `test sources section has additional sources title`() {
|
||||
val panel = panel()
|
||||
flushUntil { sourceRows(panel).size == 2 }
|
||||
|
||||
assertTrue(edt {
|
||||
components(panel).filterIsInstance<TitledSeparator>().any { it.text == "Additional Skill Sources" }
|
||||
})
|
||||
}
|
||||
|
||||
fun `test skills list does not show description tooltips`() {
|
||||
val panel = panel()
|
||||
flushUntil { rows(panel).size == 3 }
|
||||
|
||||
edt {
|
||||
val list = skillsList(panel)
|
||||
list.size = Dimension(520, 320)
|
||||
list.doLayout()
|
||||
val bounds = list.getCellBounds(0, 0)
|
||||
|
||||
assertNull(list.getToolTipText(mouse(list, MouseEvent.MOUSE_MOVED, Point(bounds.x + 8, bounds.y + 8))))
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun `test renderer puts location on first line and description on preview line`() {
|
||||
val panel = panel()
|
||||
flushUntil { rows(panel).size == 3 }
|
||||
|
||||
edt {
|
||||
val list = skillsList(panel)
|
||||
val row = rows(panel).single { it.key == CUSTOM }
|
||||
val idx = rows(panel).indexOf(row)
|
||||
val comp = list.cellRenderer.getListCellRendererComponent(list, row, idx, true, true)
|
||||
comp.setSize(520, list.fixedCellHeight)
|
||||
layout(comp)
|
||||
val title = components(comp).filterIsInstance<SimpleColoredComponent>().single()
|
||||
val labels = components(comp).filterIsInstance<JBLabel>().filter { it.isVisible }.map { it.text }
|
||||
|
||||
assertEquals("plan $CUSTOM", title.toString())
|
||||
assertTrue(labels.contains("Plan work"))
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun `test double click stages skill content until apply`() {
|
||||
val panel = panel(edit = { _, _ -> FakeSkillDialog("# Saved") })
|
||||
flushUntil { rows(panel).size == 3 }
|
||||
|
||||
doubleClick(skillsList(panel), panel, CUSTOM)
|
||||
|
||||
assertTrue(edt { panel.modified() })
|
||||
assertTrue(agentRpc.skillSaves.isEmpty())
|
||||
edt { panel.applyDraft(); true }
|
||||
flushUntil { agentRpc.skillSaves.size == 1 }
|
||||
assertEquals(Triple(DIR, CUSTOM, "# Saved"), agentRpc.skillSaves.single())
|
||||
}
|
||||
|
||||
fun `test edited skill row keeps normal actions`() {
|
||||
val panel = panel(edit = { _, _ -> FakeSkillDialog("# Draft") })
|
||||
flushUntil { rows(panel).size == 3 }
|
||||
|
||||
doubleClick(skillsList(panel), panel, CUSTOM)
|
||||
|
||||
assertEquals(listOf("open", "edit", "delete"), edt { rows(panel).single { it.key == CUSTOM }.cells.map { it.id } })
|
||||
assertTrue(edt { panel.modified() })
|
||||
}
|
||||
|
||||
fun `test reopening staged skill edit shows draft content before apply`() {
|
||||
val seen = mutableListOf<String?>()
|
||||
val panel = panel(edit = { skill, _ ->
|
||||
seen += skill.content
|
||||
FakeSkillDialog(if (seen.size == 1) "# Draft" else "# Draft 2")
|
||||
})
|
||||
flushUntil { rows(panel).size == 3 }
|
||||
|
||||
doubleClick(skillsList(panel), panel, CUSTOM)
|
||||
doubleClick(skillsList(panel), panel, CUSTOM)
|
||||
|
||||
assertEquals(listOf("# Plan\nUse steps", "# Draft"), seen)
|
||||
assertTrue(agentRpc.skillSaves.isEmpty())
|
||||
}
|
||||
|
||||
fun `test open in editor action opens skill file`() {
|
||||
val panel = panel()
|
||||
flushUntil { rows(panel).size == 3 }
|
||||
|
||||
click(skillsList(panel), panel, CUSTOM, "open")
|
||||
|
||||
assertEquals("The skill file will open after you close Settings.", edt { progressText(panel) })
|
||||
flushUntil { workspaceRpc.openedFiles.size == 1 }
|
||||
assertEquals(FakeWorkspaceRpcApi.Opened(CUSTOM, null, null), workspaceRpc.openedFiles.single())
|
||||
}
|
||||
|
||||
fun `test read only skills open without staging edits or editor file open`() {
|
||||
shown = 0
|
||||
val panel = panel(edit = { _, savable ->
|
||||
assertFalse(savable)
|
||||
FakeSkillDialog("# Ignored") { shown += 1 }
|
||||
})
|
||||
flushUntil { rows(panel).size == 3 }
|
||||
|
||||
click(skillsList(panel), panel, REMOTE, "edit")
|
||||
|
||||
assertEquals(1, shown)
|
||||
assertFalse(edt { panel.modified() })
|
||||
assertTrue(agentRpc.skillSaves.isEmpty())
|
||||
assertTrue(workspaceRpc.openedFiles.isEmpty())
|
||||
}
|
||||
|
||||
fun `test skill edit dialog shows content with fallback`() {
|
||||
edt {
|
||||
val content = SkillEditDialog(SkillDto("plan", "desc", CUSTOM, "# Plan\nUse steps"), true)
|
||||
val fallback = SkillEditDialog(SkillDto("plan", "desc", CUSTOM), true)
|
||||
val readonly = SkillEditDialog(SkillDto("kilo-config", "desc", "builtin", "<h1>Kilo Config</h1>"), false)
|
||||
try {
|
||||
assertEquals("# Plan\nUse steps", content.content())
|
||||
assertEquals("desc", fallback.content())
|
||||
assertEquals("<h1>Kilo Config</h1>", readonly.content())
|
||||
assertEquals("OK", content.okText())
|
||||
} finally {
|
||||
content.close(DialogWrapper.CANCEL_EXIT_CODE)
|
||||
fallback.close(DialogWrapper.CANCEL_EXIT_CODE)
|
||||
readonly.close(DialogWrapper.CANCEL_EXIT_CODE)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun `test skill editor file type follows content syntax before location`() {
|
||||
assertEquals(
|
||||
FileTypeManager.getInstance().getFileTypeByFileName("index.html"),
|
||||
skillFileType("builtin", "<h1>Kilo CLI Configuration Reference</h1><p>All config lives in <code>kilo.json</code>.</p>"),
|
||||
)
|
||||
assertEquals(
|
||||
skillFileType("SKILL.md"),
|
||||
skillFileType("builtin", "# Kilo CLI Configuration Reference\n\nAll config lives in `kilo.json`."),
|
||||
)
|
||||
assertEquals(PlainTextFileType.INSTANCE, skillFileType("builtin", "Plain fallback text"))
|
||||
}
|
||||
|
||||
|
||||
fun `test delete action stages skill removal until apply`() {
|
||||
val panel = panel()
|
||||
flushUntil { rows(panel).size == 3 }
|
||||
TestDialogManager.setTestDialog(TestDialog.YES)
|
||||
|
||||
click(skillsList(panel), panel, CUSTOM, "delete")
|
||||
|
||||
assertTrue(edt { rows(panel).none { it.key == CUSTOM } })
|
||||
assertTrue(agentRpc.skillRemovals.isEmpty())
|
||||
edt { panel.applyDraft(); true }
|
||||
flushUntil { agentRpc.skillRemovals.size == 1 }
|
||||
assertEquals(listOf(DIR to CUSTOM), agentRpc.skillRemovals)
|
||||
}
|
||||
|
||||
fun `test delete action requires confirmation`() {
|
||||
val panel = panel()
|
||||
flushUntil { rows(panel).size == 3 }
|
||||
TestDialogManager.setTestDialog { Messages.NO }
|
||||
|
||||
click(skillsList(panel), panel, CUSTOM, "delete")
|
||||
|
||||
edt { UIUtil.dispatchAllInvocationEvents(); true }
|
||||
assertTrue(agentRpc.skillRemovals.isEmpty())
|
||||
assertTrue(edt { rows(panel).any { it.key == CUSTOM } })
|
||||
}
|
||||
|
||||
fun `test add path and url write skills config patch on apply`() {
|
||||
var path = "/extra/skills"
|
||||
var url = "https://skills.test/index.json"
|
||||
val panel = panel(choose = { path }, input = { _, _ -> url })
|
||||
flushUntil { rows(panel).size == 3 }
|
||||
|
||||
edt { panel.sources.addPath(); true }
|
||||
edt { panel.sources.addUrl(); true }
|
||||
flushUntil { sourceRows(panel).any { it.key == "url:$url" } }
|
||||
assertTrue(appRpc.configPatches.isEmpty())
|
||||
|
||||
edt { panel.applyDraft(); true }
|
||||
flushUntil { appRpc.configPatches.size == 1 && !edt { panel.modified() } }
|
||||
|
||||
val paths = appRpc.configPatches.single().skills!!.paths
|
||||
val urls = appRpc.configPatches.single().skills!!.urls
|
||||
assertEquals(listOf("/global/skills", path), paths)
|
||||
assertEquals(listOf("https://skills.test/base.json", url), urls)
|
||||
assertEquals(
|
||||
listOf("path:/global/skills", "path:$path", "url:https://skills.test/base.json", "url:$url"),
|
||||
edt { sourceRows(panel).map { it.key } },
|
||||
)
|
||||
assertEquals(listOf(DIR), agentRpc.skillReloads)
|
||||
}
|
||||
|
||||
fun `test stale config update result keeps added skill sources visible`() {
|
||||
val path = "/extra/skills"
|
||||
val url = "https://skills.test/index.json"
|
||||
val extra = "$path/extra/SKILL.md"
|
||||
val panel = panel(choose = { path }, input = { _, _ -> url })
|
||||
appRpc.configUpdateReturnStale = true
|
||||
appRpc.afterConfig = { agentRpc.skills = agentRpc.skills + SkillDto("extra", "Extra skill", extra) }
|
||||
flushUntil { rows(panel).size == 3 }
|
||||
|
||||
edt {
|
||||
panel.sources.addPath()
|
||||
panel.sources.addUrl()
|
||||
panel.applyDraft()
|
||||
true
|
||||
}
|
||||
|
||||
flushUntil { appRpc.configPatches.size == 1 && !edt { panel.modified() } }
|
||||
assertTrue(edt { rows(panel).any { it.key == extra } })
|
||||
assertEquals(
|
||||
listOf("path:/global/skills", "path:$path", "url:https://skills.test/base.json", "url:$url"),
|
||||
edt { sourceRows(panel).map { it.key } },
|
||||
)
|
||||
}
|
||||
|
||||
fun `test blocked reload completes apply with warning`() {
|
||||
val path = "/extra/skills"
|
||||
val panel = panel(choose = { path })
|
||||
agentRpc.reloadSkillResult = false
|
||||
flushUntil { rows(panel).size == 3 }
|
||||
|
||||
edt {
|
||||
panel.sources.addPath()
|
||||
panel.applyDraft()
|
||||
true
|
||||
}
|
||||
|
||||
flushUntil { appRpc.configPatches.size == 1 && !edt { panel.modified() } }
|
||||
assertEquals(listOf(DIR), agentRpc.skillReloads)
|
||||
assertEquals("Skills settings saved, but active sessions are present. Reload the core after those sessions finish to apply the new skills.", edt { progressText(panel) })
|
||||
}
|
||||
|
||||
fun `test post apply skills refresh failure keeps saved rows`() {
|
||||
val panel = panel(edit = { _, _ -> FakeSkillDialog("# Saved") })
|
||||
flushUntil { rows(panel).size == 3 }
|
||||
|
||||
doubleClick(skillsList(panel), panel, CUSTOM)
|
||||
agentRpc.skillsError = RuntimeException("timeout")
|
||||
edt { panel.applyDraft(); true }
|
||||
|
||||
flushUntil { agentRpc.skillSaves.size == 1 && !edt { panel.modified() } }
|
||||
assertEquals(listOf(CUSTOM, "builtin", REMOTE), edt { rows(panel).map { it.key } })
|
||||
assertEquals("# Saved", agentRpc.skills.single { it.location == CUSTOM }.content)
|
||||
}
|
||||
|
||||
fun `test source reset discards staged changes`() {
|
||||
val path = "/extra/skills"
|
||||
val panel = panel(choose = { path })
|
||||
flushUntil { rows(panel).size == 3 }
|
||||
|
||||
edt { panel.sources.addPath(); true }
|
||||
|
||||
assertTrue(edt { sourceRows(panel).any { it.key == "path:$path" } })
|
||||
assertTrue(edt { panel.modified() })
|
||||
edt { panel.resetDraft(); true }
|
||||
|
||||
assertTrue(appRpc.configPatches.isEmpty())
|
||||
assertEquals(listOf(CUSTOM, "builtin", REMOTE), edt { rows(panel).map { it.key } })
|
||||
assertFalse(edt { sourceRows(panel).any { it.key == "path:$path" } })
|
||||
assertTrue(agentRpc.skillReloads.isEmpty())
|
||||
}
|
||||
|
||||
fun `test delete source writes skills config patch`() {
|
||||
val panel = panel()
|
||||
flushUntil { rows(panel).size == 3 && sourceRows(panel).size == 2 }
|
||||
|
||||
edt {
|
||||
sourceList(panel).selectedIndices = intArrayOf(0)
|
||||
panel.sources.removeSelected()
|
||||
true
|
||||
}
|
||||
|
||||
assertTrue(appRpc.configPatches.isEmpty())
|
||||
edt { panel.applyDraft(); true }
|
||||
flushUntil { appRpc.configPatches.size == 1 && !edt { panel.modified() } }
|
||||
val patch = appRpc.configPatches.single().skills!!
|
||||
assertEquals(emptyList<String>(), patch.paths)
|
||||
assertEquals(listOf("https://skills.test/base.json"), patch.urls)
|
||||
assertEquals(listOf("url:https://skills.test/base.json"), edt { sourceRows(panel).map { it.key } })
|
||||
assertEquals(listOf(DIR), agentRpc.skillReloads)
|
||||
}
|
||||
|
||||
fun `test stale config update result keeps removed skill sources hidden`() {
|
||||
val panel = panel()
|
||||
appRpc.configUpdateReturnStale = true
|
||||
appRpc.afterConfig = { agentRpc.skills = agentRpc.skills.filterNot { it.location == CUSTOM } }
|
||||
flushUntil { rows(panel).size == 3 && sourceRows(panel).size == 2 }
|
||||
|
||||
edt {
|
||||
sourceList(panel).selectedIndices = intArrayOf(0)
|
||||
panel.sources.removeSelected()
|
||||
panel.applyDraft()
|
||||
true
|
||||
}
|
||||
|
||||
flushUntil { appRpc.configPatches.size == 1 && !edt { panel.modified() } }
|
||||
assertEquals(listOf("builtin", REMOTE), edt { rows(panel).map { it.key } })
|
||||
assertEquals(listOf("url:https://skills.test/base.json"), edt { sourceRows(panel).map { it.key } })
|
||||
}
|
||||
|
||||
fun `test search filters skills by name`() {
|
||||
val panel = panel()
|
||||
flushUntil { rows(panel).size == 3 }
|
||||
|
||||
edt {
|
||||
components(panel).filterIsInstance<JTextField>().single().text = "think"
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
true
|
||||
}
|
||||
|
||||
flushUntil { rows(panel).map { it.key } == listOf("builtin") }
|
||||
}
|
||||
|
||||
fun `test skills reload failure keeps existing rows`() {
|
||||
val panel = panel()
|
||||
flushUntil { rows(panel).size == 3 }
|
||||
agentRpc.skillsError = RuntimeException("timeout")
|
||||
|
||||
edt { panel.reload(); true }
|
||||
flushUntil { edt { skillsList(panel).isEnabled } }
|
||||
|
||||
assertEquals(listOf(CUSTOM, "builtin", REMOTE), edt { rows(panel).map { it.key } })
|
||||
}
|
||||
|
||||
fun `test skill editor file type follows location extension`() {
|
||||
assertNotSame(UnknownFileType.INSTANCE, skillFileType("/tmp/skills/plan/SKILL.md"))
|
||||
assertEquals(
|
||||
FileTypeManager.getInstance().getFileTypeByFileName("index.html"),
|
||||
skillFileType("/tmp/skills/index.html"),
|
||||
)
|
||||
assertEquals(PlainTextFileType.INSTANCE, skillFileType("/tmp/skills/index.unknown"))
|
||||
}
|
||||
|
||||
fun `test skill path chooser accepts directories only`() {
|
||||
val descriptor = skillPathDescriptor()
|
||||
|
||||
assertTrue(descriptor.isChooseFolders)
|
||||
assertFalse(descriptor.isChooseFiles)
|
||||
}
|
||||
|
||||
private fun panel(
|
||||
choose: (JComponent) -> String? = { null },
|
||||
input: (String, String) -> String? = { _, _ -> null },
|
||||
edit: (SkillDto, Boolean) -> SkillEditDialogHandle = { _, _ -> FakeSkillDialog("# Plan\nUse steps") },
|
||||
): SkillsSettingsUi {
|
||||
install()
|
||||
val panel = edt { SkillsSettingsUi(scope!!, DIR, choose, input, edit) }
|
||||
ui = panel
|
||||
edt { panel.reload(); true }
|
||||
return panel
|
||||
}
|
||||
|
||||
private fun install() {
|
||||
val cs = CoroutineScope(SupervisorJob())
|
||||
scope = cs
|
||||
appRpc = FakeAppRpcApi()
|
||||
workspaceRpc = FakeWorkspaceRpcApi()
|
||||
agentRpc = FakeAgentBehaviorRpcApi().apply {
|
||||
skills = listOf(
|
||||
SkillDto("plan", "Plan work", CUSTOM, "# Plan\nUse steps", editable = true),
|
||||
SkillDto("thinking", "Built in", "builtin", "Built in content"),
|
||||
SkillDto("remote", "Remote skill", REMOTE, "# Remote skill"),
|
||||
)
|
||||
}
|
||||
app = KiloAppService(cs, appRpc)
|
||||
val ready = KiloAppStateDto(
|
||||
KiloAppStatusDto.READY,
|
||||
config = ConfigDto(skills = SkillsConfigDto(
|
||||
paths = listOf("/global/skills"),
|
||||
urls = listOf("https://skills.test/base.json"),
|
||||
)),
|
||||
)
|
||||
app._state.value = ready
|
||||
appRpc.state.value = ready
|
||||
ApplicationManager.getApplication().replaceService(KiloAppService::class.java, app, testRootDisposable)
|
||||
ApplicationManager.getApplication().replaceService(KiloAgentBehaviorService::class.java, KiloAgentBehaviorService(cs, agentRpc), testRootDisposable)
|
||||
ApplicationManager.getApplication().replaceService(KiloWorkspaceService::class.java, KiloWorkspaceService(cs, workspaceRpc), testRootDisposable)
|
||||
}
|
||||
|
||||
private fun click(list: JBList<SettingsListItem>, panel: SkillsSettingsUi, key: String, id: String) {
|
||||
edt {
|
||||
list.size = Dimension(520, 320)
|
||||
list.doLayout()
|
||||
val rows = if (list === skillsList(panel)) rows(panel) else sourceRows(panel)
|
||||
val idx = rows.indexOfFirst { it.key == key }
|
||||
list.selectedIndex = idx
|
||||
val area = settingsListCellBounds(list, idx, selected = true).getValue(id)
|
||||
click(list, center(area))
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun doubleClick(list: JBList<SettingsListItem>, panel: SkillsSettingsUi, key: String) {
|
||||
edt {
|
||||
list.size = Dimension(520, 320)
|
||||
list.doLayout()
|
||||
val idx = rows(panel).indexOfFirst { it.key == key }
|
||||
list.selectedIndex = idx
|
||||
val area = list.getCellBounds(idx, idx)
|
||||
fire(list, mouse(list, MouseEvent.MOUSE_CLICKED, center(area), count = 2))
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun rows(panel: SkillsSettingsUi): List<SettingsListItem> = items(skillsList(panel))
|
||||
|
||||
private fun sourceRows(panel: SkillsSettingsUi): List<SettingsListItem> = items(sourceList(panel))
|
||||
|
||||
private fun items(list: JBList<SettingsListItem>): List<SettingsListItem> {
|
||||
val model = list.model
|
||||
return (0 until model.size).map { model.getElementAt(it) }
|
||||
}
|
||||
|
||||
private fun skillsList(panel: SkillsSettingsUi) = components(panel).filterIsInstance<JBList<SettingsListItem>>().first()
|
||||
|
||||
private fun sourceList(panel: SkillsSettingsUi) = components(panel).filterIsInstance<JBList<SettingsListItem>>().last()
|
||||
|
||||
private fun scrollFor(panel: SkillsSettingsUi, list: JBList<SettingsListItem>) = components(panel)
|
||||
.filterIsInstance<JBScrollPane>()
|
||||
.single { pane -> pane.viewport.view === list.parent }
|
||||
|
||||
private fun progressText(panel: SkillsSettingsUi) = components(panel.progress).filterIsInstance<JBLabel>().single().text
|
||||
|
||||
private fun SkillEditDialog.okText(): String {
|
||||
val method = DialogWrapper::class.java.getDeclaredMethod("getOKAction")
|
||||
method.isAccessible = true
|
||||
return (method.invoke(this) as javax.swing.Action).getValue(javax.swing.Action.NAME) as String
|
||||
}
|
||||
|
||||
private fun components(root: java.awt.Component): List<java.awt.Component> {
|
||||
val out = mutableListOf<java.awt.Component>()
|
||||
fun visit(item: java.awt.Component) {
|
||||
out += item
|
||||
if (item is Container) item.components.forEach { visit(it) }
|
||||
}
|
||||
visit(root)
|
||||
return out
|
||||
}
|
||||
|
||||
private fun layout(root: java.awt.Component) {
|
||||
root.doLayout()
|
||||
if (root is Container) root.components.filterIsInstance<Container>().forEach { layout(it) }
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
}
|
||||
|
||||
private fun center(rect: java.awt.Rectangle) = Point(rect.x + rect.width / 2, rect.y + rect.height / 2)
|
||||
|
||||
private fun click(list: JBList<SettingsListItem>, point: Point) {
|
||||
fire(list, mouse(list, MouseEvent.MOUSE_PRESSED, point))
|
||||
fire(list, mouse(list, MouseEvent.MOUSE_RELEASED, point))
|
||||
}
|
||||
|
||||
private fun mouse(list: JBList<SettingsListItem>, id: Int, point: Point, count: Int = 1) = MouseEvent(
|
||||
list,
|
||||
id,
|
||||
System.currentTimeMillis(),
|
||||
if (id == MouseEvent.MOUSE_PRESSED) InputEvent.BUTTON1_DOWN_MASK else 0,
|
||||
point.x,
|
||||
point.y,
|
||||
count,
|
||||
false,
|
||||
MouseEvent.BUTTON1,
|
||||
)
|
||||
|
||||
private fun <T> edt(block: () -> T): T {
|
||||
var result: T? = null
|
||||
ApplicationManager.getApplication().invokeAndWait { result = block() }
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return result as T
|
||||
}
|
||||
|
||||
private fun flushUntil(done: () -> Boolean) = runBlocking {
|
||||
repeat(300) {
|
||||
delay(10)
|
||||
edt { UIUtil.dispatchAllInvocationEvents(); true }
|
||||
if (done()) return@runBlocking
|
||||
}
|
||||
edt { UIUtil.dispatchAllInvocationEvents(); true }
|
||||
assertTrue(done())
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val DIR = "/test"
|
||||
const val CUSTOM = "/home/test/.config/kilo/skill/plan/SKILL.md"
|
||||
const val REMOTE = "/home/test/.cache/kilo/skills/remote/SKILL.md"
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeSkillDialog(private val text: String, private val show: () -> Unit = {}) : SkillEditDialogHandle {
|
||||
override fun showAndGet(): Boolean {
|
||||
show()
|
||||
return true
|
||||
}
|
||||
override fun content() = text
|
||||
}
|
||||
+33
@@ -14,6 +14,9 @@ import java.awt.Dimension
|
||||
import java.awt.Point
|
||||
import java.awt.event.InputEvent
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.ListSelectionModel
|
||||
import javax.swing.Scrollable
|
||||
import javax.swing.SwingConstants
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
class SettingsListViewTest : BasePlatformTestCase() {
|
||||
@@ -236,6 +239,25 @@ class SettingsListViewTest : BasePlatformTestCase() {
|
||||
}
|
||||
}
|
||||
|
||||
fun `test action click invokes on second selected row in multi selection list`() {
|
||||
edt {
|
||||
val calls = mutableListOf<String>()
|
||||
val cfg = SettingsListConfig.Equal.copy(selection = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
|
||||
val view = SettingsListView("Empty", cfg) { key, id -> calls += "$key:$id" }
|
||||
view.update(listOf(
|
||||
item("a", "Alpha", null, SettingsListCell("edit", "Edit", alwaysVisible = false)),
|
||||
item("b", "Beta", null, SettingsListCell("edit", "Edit", alwaysVisible = false)),
|
||||
))
|
||||
layout(view)
|
||||
view.list.selectedIndices = intArrayOf(0, 1)
|
||||
|
||||
val area = settingsListCellBounds(view.list, 1, selected = true).getValue("edit")
|
||||
click(view, center(area))
|
||||
|
||||
assertEquals(listOf("b:edit"), calls)
|
||||
}
|
||||
}
|
||||
|
||||
fun `test update selects preferred key`() {
|
||||
edt {
|
||||
val view = SettingsListView("Empty") { _, _ -> }
|
||||
@@ -260,6 +282,17 @@ class SettingsListViewTest : BasePlatformTestCase() {
|
||||
}
|
||||
}
|
||||
|
||||
fun `test list view tracks viewport width`() {
|
||||
edt {
|
||||
val view = SettingsListView("Empty") { _, _ -> }
|
||||
view.update(listOf(item("long", "Alpha", "A very long description that should wrap instead of scrolling")))
|
||||
|
||||
assertTrue((view as Scrollable).getScrollableTracksViewportWidth())
|
||||
assertFalse(view.getScrollableTracksViewportHeight())
|
||||
assertEquals(160, view.getScrollableBlockIncrement(java.awt.Rectangle(0, 0, 320, 160), SwingConstants.VERTICAL, 1))
|
||||
}
|
||||
}
|
||||
|
||||
private fun item(id: String, name: String, note: String?, vararg cells: SettingsListCell) = object : SettingsListItem {
|
||||
override val key = id
|
||||
override val title = name
|
||||
|
||||
+42
-2
@@ -11,9 +11,14 @@ import ai.kilocode.rpc.dto.SkillDto
|
||||
|
||||
class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi {
|
||||
var agents = emptyList<AgentDetailDto>()
|
||||
var skills = emptyList<SkillDto>()
|
||||
var mcps = emptyList<McpStatusDto>()
|
||||
var mcpConfigs = emptyMap<String, McpServerConfigDto>()
|
||||
val agentCalls = mutableListOf<String>()
|
||||
val skillCalls = mutableListOf<String>()
|
||||
val skillRemovals = mutableListOf<Pair<String, String>>()
|
||||
val skillReloads = mutableListOf<String>()
|
||||
val skillSaves = mutableListOf<Triple<String, String, String>>()
|
||||
val mcpCalls = mutableListOf<String>()
|
||||
val mcpConfigCalls = mutableListOf<String>()
|
||||
val mcpSaves = mutableListOf<Triple<String, String, McpConfigDto?>>()
|
||||
@@ -27,10 +32,16 @@ class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi {
|
||||
var afterRemove: (suspend (String, String) -> Unit)? = null
|
||||
var afterMcpConnect: (suspend (String, String) -> Unit)? = null
|
||||
var createError: Exception? = null
|
||||
var skillsError: Exception? = null
|
||||
var removeError: Exception? = null
|
||||
var removeSkillError: Exception? = null
|
||||
var saveSkillError: Exception? = null
|
||||
var mcpStatusError: Exception? = null
|
||||
var mcpConnectError: Exception? = null
|
||||
var removeResult = true
|
||||
var removeSkillResult = true
|
||||
var reloadSkillResult = true
|
||||
var saveSkillResult = true
|
||||
var mcpConnectResult = true
|
||||
var mcpDisconnectResult = true
|
||||
var mcpAuthenticateResult = true
|
||||
@@ -43,12 +54,41 @@ class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi {
|
||||
|
||||
override suspend fun skills(directory: String): List<SkillDto> {
|
||||
assertNotEdt("agentBehavior.skills")
|
||||
return emptyList()
|
||||
skillsError?.let { throw it }
|
||||
skillCalls.add(directory)
|
||||
return skills
|
||||
}
|
||||
|
||||
override suspend fun removeSkill(directory: String, location: String): Boolean {
|
||||
assertNotEdt("agentBehavior.removeSkill")
|
||||
return false
|
||||
removeSkillError?.let { throw it }
|
||||
skillRemovals.add(directory to location)
|
||||
if (removeSkillResult) skills = skills.filterNot { it.location == location }
|
||||
return removeSkillResult
|
||||
}
|
||||
|
||||
override suspend fun reloadSkills(directory: String): Boolean {
|
||||
assertNotEdt("agentBehavior.reloadSkills")
|
||||
skillReloads.add(directory)
|
||||
return reloadSkillResult
|
||||
}
|
||||
|
||||
override suspend fun saveSkill(directory: String, location: String, content: String): Boolean {
|
||||
assertNotEdt("agentBehavior.saveSkill")
|
||||
saveSkillError?.let { throw it }
|
||||
skillSaves.add(Triple(directory, location, content))
|
||||
if (saveSkillResult) skills = skills.map { if (it.location == location) it.copy(content = content) else it }
|
||||
return saveSkillResult
|
||||
}
|
||||
|
||||
override suspend fun saveSkills(directory: String, edits: Map<String, String>): Boolean {
|
||||
assertNotEdt("agentBehavior.saveSkills")
|
||||
saveSkillError?.let { throw it }
|
||||
for ((location, content) in edits) skillSaves.add(Triple(directory, location, content))
|
||||
if (saveSkillResult) skills = skills.map { skill ->
|
||||
edits[skill.location]?.let { skill.copy(content = it) } ?: skill
|
||||
}
|
||||
return saveSkillResult
|
||||
}
|
||||
|
||||
override suspend fun removeAgent(directory: String, name: String): Boolean {
|
||||
|
||||
+6
@@ -26,6 +26,12 @@ interface KiloAgentBehaviorRpcApi : RemoteApi<Unit> {
|
||||
|
||||
suspend fun removeSkill(directory: String, location: String): Boolean
|
||||
|
||||
suspend fun reloadSkills(directory: String): Boolean
|
||||
|
||||
suspend fun saveSkill(directory: String, location: String, content: String): Boolean
|
||||
|
||||
suspend fun saveSkills(directory: String, edits: Map<String, String>): Boolean
|
||||
|
||||
suspend fun removeAgent(directory: String, name: String): Boolean
|
||||
|
||||
suspend fun createAgent(directory: String, input: AgentCreateDto): Boolean
|
||||
|
||||
@@ -7,4 +7,6 @@ data class SkillDto(
|
||||
val name: String,
|
||||
val description: String? = null,
|
||||
val location: String,
|
||||
val content: String? = null,
|
||||
val editable: Boolean = false,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user