Merge remote-tracking branch 'origin/main' into marius-kilocode/kilo-opencode-v1.17.5

This commit is contained in:
marius-kilocode
2026-07-21 12:35:20 +02:00
259 changed files with 6952 additions and 2489 deletions
-6
View File
@@ -1,6 +0,0 @@
---
"@kilocode/cli": minor
"kilo-code": minor
---
Allow agents to stop and remove a targeted Agent Manager session.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/cli": patch
---
Improve CLI sidebar usage sections with cent-formatted costs, collapsible details, and aligned model totals.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Respect the Terminal Command Blocks display setting for background process tool cards.
-6
View File
@@ -1,6 +0,0 @@
---
"@kilocode/cli": patch
"kilo-code": patch
---
Display here-document content as plain text in terminal approval prompts.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Support bidirectional text in chat messages so right-to-left and mixed-language conversations render in the correct direction.
-6
View File
@@ -1,6 +0,0 @@
---
"@kilocode/cli": patch
"@kilocode/kilo-gateway": patch
---
Fix Cloud Agent session imports in installed CLI builds and prevent malformed exports or write failures from leaving partial imports.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Open the Agent Manager new-worktree dialog from primary actions and keyboard shortcuts, with an explicit quick-create action in its dropdown.
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Keep sessions usable after deprecated local review commands complete without a session status transition.
-7
View File
@@ -1,7 +0,0 @@
---
"@kilocode/cli": minor
"@kilocode/kilo-indexing": minor
"kilo-code": minor
---
Configure a custom file extension allowlist for codebase indexing to limit scans to relevant project files and support additional text formats.
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/kilo-jetbrains": minor
---
Support viewing, opening, editing, deleting, and configuring JetBrains skill sources.
+8
View File
@@ -0,0 +1,8 @@
---
"@kilocode/cli": patch
"@kilocode/kilo-memory": patch
"@kilocode/kilo-ui": patch
"kilo-code": patch
---
Simplify project memory settings and activity visibility, replace direct editing with folder inspection, add nested memory slash-command completion and status views, improve empty-project handling, compact native tool-call summaries, and remove legacy memory audit logs.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-indexing": patch
---
Allow remote embedder validation timeouts to retry before indexing reports a connection failure.
@@ -1,5 +0,0 @@
---
"@kilocode/cli": minor
---
Remote CLI: expose slash command discovery, execution, and `/new` session creation over the relay. `list_commands` and `send_command` (including the built-in `compact` flow) are scoped to the current session's directory. `create_session` creates a root session in that directory, attaches it to the relay heartbeat set, and returns the new session id only after the heartbeat completes so the mobile client can navigate immediately. Failures are sanitized; the command is not auto-retried and the user may retry manually after a transient relay failure.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Fix scroll position not resetting when switching between diff files in the chat and virtual diff viewer
+1 -1
View File
@@ -160,7 +160,7 @@ jobs:
if: matrix.settings.run if: matrix.settings.run
run: bun turbo test:ci --output-logs=errors-only --log-order=grouped --log-prefix=task --filter='@kilocode/cli' run: bun turbo test:ci --output-logs=errors-only --log-order=grouped --log-prefix=task --filter='@kilocode/cli'
env: env:
KILO_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }} KILO_EXPERIMENTAL_DISABLE_FILEWATCHER: "true" # kilocode_change - was Windows-only; the CLI now starts a watcher per instance, too heavy/racy for unit tests. Watcher tests opt back in.
KILO_TEST_PROFILE: ${{ matrix.settings.os == 'macos' && 'darwin' || '' }} KILO_TEST_PROFILE: ${{ matrix.settings.os == 'macos' && 'darwin' || '' }}
KILO_TEST_SHARD: ${{ format('{0}/{1}', matrix.settings.index, matrix.settings.total) }} KILO_TEST_SHARD: ${{ format('{0}/{1}', matrix.settings.index, matrix.settings.total) }}
# kilocode_change end # kilocode_change end
@@ -0,0 +1,423 @@
# JetBrains Context Settings Page
Implement the Tier 1 Context settings from `docs/jetbrains-vscode-settings-parity.md` in the JetBrains plugin. This is a pure `kilo.json` settings UI: no CLI feature work, no SDK regen, and no session-rendering changes.
## Goal
Add a new JetBrains settings page under `Settings -> Tools -> Kilo Code -> Context` for:
| Setting | Config key | Type |
|---|---|---|
| Auto-compaction | `compaction.auto` | boolean |
| Compaction threshold percent | `compaction.threshold_percent` | number or null |
| Prune on compaction | `compaction.prune` | boolean |
| Watcher ignore patterns | `watcher.ignore` | string array |
Do not include VS Code Context-tab memory/indexing controls in this first pass. JetBrains does not have the equivalent memory/indexing settings service yet, and the parity doc excludes indexing from easy wins.
Do not put `snapshot` on this page unless product explicitly decides to combine Context and Checkpoints. The parity doc suggests `snapshot` belongs on a new Checkpoints page.
## Context Verified
- Source parity doc: `docs/jetbrains-vscode-settings-parity.md`.
- JetBrains settings guidance: `packages/kilo-jetbrains/AGENTS.md`, especially `Settings UI`.
- Existing settings pages are registered in `packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml`.
- Existing page pattern to mirror:
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsConfigurable.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsState.kt`
- Existing global config write path is sufficient once DTO/parser support is added:
- Frontend: `KiloAppService.updateConfigAsync(...)`
- RPC: `KiloAppRpcApi.updateConfig(patch: ConfigPatchDto)`
- Backend: `KiloBackendAppService.updateConfig(...)`
- HTTP: `PATCH /global/config`, then `GET /global/config`
- Existing backend parser currently only serializes selected string keys from `ConfigPatchDto.values`; Context needs typed booleans, numbers, explicit null, and string arrays.
## Decisions
- Use global config for the first implementation, matching the existing app-level settings write path.
- Add typed DTO fields instead of overloading `ConfigPatchDto.values` for non-string values.
- Use an explicit `clear` list for nullable compaction fields, because `Double?` cannot distinguish absent from explicit `null`.
- Reuse `BaseSettingsUi`, `DraftReadyConfigurable`, `SettingsDraftState`, `SettingsRows`, `SettingsRow`, and `SettingsToggle`.
- Use the shared settings list primitives for `watcher.ignore`; do not build a bespoke add/remove list if `SettingsListPanel` or adjacent list primitives fit.
- Keep all UI strings in `KiloBundle.properties`. Let other locale bundles fall back unless the repo's resource-bundle checks require duplicated English keys.
## Part A - Shared DTOs
File: `packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/KiloAppStateDto.kt`
Add config read DTOs:
```kotlin
@Serializable
data class WatcherConfigDto(
val ignore: List<String> = emptyList(),
)
@Serializable
data class CompactionConfigDto(
val auto: Boolean? = null,
val threshold_percent: Double? = null,
val prune: Boolean? = null,
)
```
Extend `ConfigDto`:
```kotlin
val watcher: WatcherConfigDto? = null,
val compaction: CompactionConfigDto? = null,
```
Add patch DTOs:
```kotlin
@Serializable
data class WatcherPatchDto(
val ignore: List<String>? = null,
)
@Serializable
data class CompactionPatchDto(
val clear: List<String> = emptyList(),
val auto: Boolean? = null,
val threshold_percent: Double? = null,
val prune: Boolean? = null,
)
```
Extend `ConfigPatchDto`:
```kotlin
val watcher: WatcherPatchDto? = null,
val compaction: CompactionPatchDto? = null,
```
Notes:
- `watcher.ignore = null` means no change.
- `watcher.ignore = emptyList()` means explicitly save an empty list.
- `compaction.threshold_percent = null` alone means no change.
- `compaction.clear = listOf("threshold_percent")` means emit JSON `"threshold_percent": null`.
- `false` boolean values must be serialized; do not treat `false` as absent.
## Part B - Backend Config Parser And Serializer
File: `packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt`
### Parse
Extend `parseConfig(raw)` to read:
- `watcher.ignore`
- `compaction.auto`
- `compaction.threshold_percent`
- `compaction.prune`
Add private helpers near `parseSkillsConfig` / `parseMcpConfig`:
```kotlin
private fun parseWatcherConfig(obj: JsonObject?): WatcherConfigDto?
private fun parseCompactionConfig(obj: JsonObject?): CompactionConfigDto?
```
Use existing helper style:
- strings: `str(...)`
- booleans: `flagOrNull(...)`
- numbers: `num(...)`
- arrays: `arr()?.mapNotNull { it.jsonPrimitive.contentOrNull }`
### Serialize
Extend `buildConfigPatch(patch)` to emit typed context patches:
```json
{
"watcher": {
"ignore": ["**/node_modules/**"]
},
"compaction": {
"auto": true,
"threshold_percent": 80,
"prune": false
}
}
```
For explicit threshold clearing:
```json
{
"compaction": {
"threshold_percent": null
}
}
```
Keep the existing `values` allowlist for string model keys. Do not pass Context values through `values`.
## Part C - Frontend State Model
Add package: `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/context/`
New file: `ContextSettingsState.kt`
Define:
```kotlin
internal data class ContextDraft(
val auto: Boolean? = null,
val threshold: String = "",
val prune: Boolean? = null,
val ignore: List<String> = emptyList(),
)
```
Use a string for the threshold draft so the UI can represent blank/invalid intermediate input without losing user text. Convert only when building a patch.
Functions to add:
- `contextDraft(config: ConfigDto?): ContextDraft`
- `patch(from: ContextDraft, to: ContextDraft): ConfigPatchDto`
- `savedMatches(base: ContextDraft, draft: ContextDraft): Boolean`
- `threshold(value: String): Double?` or equivalent parsing helper
- validation helper for threshold range if desired
Patch behavior:
- Only emit changed fields.
- Emit `CompactionPatchDto(auto = false)` when the user turns auto-compaction off.
- Emit `CompactionPatchDto(prune = false)` when the user turns pruning off.
- Emit `CompactionPatchDto(threshold_percent = 80.0)` for a non-blank valid number.
- Emit `CompactionPatchDto(clear = listOf("threshold_percent"))` when an existing threshold is cleared.
- Emit `WatcherPatchDto(ignore = emptyList())` when the last ignore pattern is removed.
- Return no change from the page when all fields match the baseline.
## Part D - Frontend UI Page
New file: `ContextConfigurable.kt`
Mirror `ModelsConfigurable`:
- Extend `DraftReadyConfigurable<JComponent>`.
- `ID = "ai.kilocode.jetbrains.settings.context"`.
- `getDisplayName()` returns `KiloBundle.message("settings.context.displayName")`.
- `create(cs)` returns `ContextSettingsUi(cs)`.
New file: `ContextSettingsUi.kt`
Mirror the simple parts of `ModelsSettingsUi`:
- Extend `BaseSettingsUi<ContextSettingsContent, ContextDraft, ConfigPatchDto, KiloAppStateDto, Unit>`.
- Initial draft is `ContextDraft()`.
- `save(change, done)` calls `app.updateConfigAsync(change, done)`.
- `base(result)` and `draft(state)` call `contextDraft(state.config)`.
- `saved(base, draft)` calls `savedMatches(base, draft)`.
- `pendingText()` uses `settings.context.save.pending`.
- `failedText()` uses `settings.context.save.failed`.
- `loadWorkspace(root)` returns `Unit`; `applyWorkspace(result)` is `Unit`.
- `models(state)` is `Unit`.
- `syncContent()` updates enabled states, field values, save/progress overlay, and validation messaging.
New content class: `ContextSettingsContent`
Suggested layout:
- Section `settings.context.compaction.title`
- Toggle row `settings.context.compaction.auto.title`
- Numeric row `settings.context.compaction.threshold.title`
- Toggle row `settings.context.compaction.prune.title`
- Section `settings.context.watcher.title`
- List editor row/panel for ignore patterns
Controls:
- Use `SettingsToggle` for booleans.
- Use `JBTextField` or a small reusable numeric field pattern based on `AgentEditDialog` for threshold.
- Use shared list primitives (`SettingsListPanel` / `SettingsListView` / `SettingsListItem` / `SettingsListCell`) for `watcher.ignore` where practical.
- Keep the page editable while app status is ready and no save is pending.
- Disable controls while saving.
Validation:
- Blank threshold is valid and means clear/reset the config value if it differs from baseline.
- Non-numeric threshold is invalid and should prevent `apply()` from sending a patch.
- Suggested accepted range is `0..100`; if existing CLI allows a broader range, follow CLI behavior.
- Show validation through existing settings messaging rather than custom ad hoc labels.
## Part E - Settings Registration And Root Navigation
File: `packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml`
Add a child configurable:
```xml
<applicationConfigurable
parentId="ai.kilocode.jetbrains.settings"
id="ai.kilocode.jetbrains.settings.context"
groupWeight="3"
instance="ai.kilocode.client.settings.context.ContextConfigurable"
bundle="messages.KiloBundle"
key="settings.context.displayName"/>
```
Adjust weights so the desired order is stable. Recommended order:
| Page | Weight |
|---|---|
| User Profile | 5 |
| Models | 4 |
| Context | 3 |
| Providers | 2 |
| Agent Behavior | 1 |
File: `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/KiloSettingsConfigurable.kt`
Add a root-page `ActionLink` for Context between Models and Providers:
- Import `ContextConfigurable`.
- Link text: `settings.context.displayName`.
- Link target: `ContextConfigurable.ID`.
`KiloSettingsSelection.kt` probably needs no code changes because child IDs already share the root prefix.
## Part F - Strings
File: `packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties`
Add base strings near the other settings strings:
```properties
settings.context.displayName=Context
settings.context.description=Configure compaction and file-watcher context behavior.
settings.context.save.pending=Saving context settings...
settings.context.save.failed=Failed to save context settings
settings.context.compaction.title=Compaction
settings.context.compaction.description=Control when Kilo summarizes long sessions to reduce context usage.
settings.context.compaction.auto.title=Auto-compaction
settings.context.compaction.auto.description=Automatically compact long conversations before they exceed the model context window.
settings.context.compaction.threshold.title=Compaction threshold
settings.context.compaction.threshold.description=Percent of the context window to use before auto-compaction starts. Leave blank to use the default.
settings.context.compaction.threshold.invalid=Enter a number from 0 to 100, or leave the field blank.
settings.context.compaction.prune.title=Prune on compaction
settings.context.compaction.prune.description=Drop older raw conversation details after compaction to keep the session context smaller.
settings.context.watcher.title=Watcher ignore patterns
settings.context.watcher.description=Glob patterns Kilo should ignore when watching repository file changes.
settings.context.watcher.add=Add pattern
settings.context.watcher.empty=No ignore patterns configured.
settings.context.watcher.placeholder=e.g. **/dist/**
settings.context.watcher.remove=Remove {0}
```
If resource-bundle tests require every key in every locale bundle, copy English values into the localized bundles and leave translation work for a later i18n pass.
## Part G - Test Updates
### Frontend state tests
Add: `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/context/ContextSettingsStateTest.kt`
Cover:
- Draft reads `ConfigDto.watcher` and `ConfigDto.compaction`.
- Unchanged draft emits no patch.
- Boolean changes emit `false` and `true` correctly.
- Threshold set emits `threshold_percent`.
- Threshold clear emits `clear = listOf("threshold_percent")`.
- Watcher list add/remove emits the whole new `ignore` list, including empty list.
- Invalid threshold is rejected before save if validation lives in state helpers.
### Frontend UI tests
Add: `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/context/ContextSettingsUiTest.kt`
Use `ModelsSettingsUiTest` as the main pattern:
- `BasePlatformTestCase`.
- Real EDT.
- `FakeAppRpcApi`.
- `KiloAppService`.
- `flushUntil` helpers.
- Assert `rpc.configPatches` after user interaction.
- Assert controls disable during pending save.
- Assert failed save leaves page modified and shows `settings.context.save.failed`.
Update `FakeAppRpcApi`:
- File: `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAppRpcApi.kt`
- Apply `patch.watcher` and `patch.compaction` to fake config state.
- Preserve explicit empty lists.
- Preserve boolean `false`.
- Honor `compaction.clear` by setting cleared fields to `null`.
### Backend parser tests
Update: `packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt`
Add exact JSON tests for:
- `parseConfig` reads watcher and compaction fields.
- `buildConfigPatch` emits watcher ignore arrays.
- `buildConfigPatch` emits `auto=false` and `prune=false`.
- `buildConfigPatch` emits numeric `threshold_percent`.
- `buildConfigPatch` emits explicit `threshold_percent:null` when `clear` includes the field.
### Backend app service tests
Update: `packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendAppServiceTest.kt`
Add a test similar to the existing model config update test:
- Call `updateConfig(ConfigPatchDto(watcher = ..., compaction = ...))`.
- Assert `MockCliServer.lastConfigPatchBody` exactly matches the expected nested JSON.
- Assert the returned/reloaded `ConfigDto` includes the saved Context values.
### Root settings tests
Update: `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/KiloSettingsConfigurableTest.kt`
Add:
- `ContextConfigurable.ID == "ai.kilocode.jetbrains.settings.context"`.
- Root page includes a Context link.
- Link order matches XML order.
## Validation
Run from `packages/kilo-jetbrains/`:
```bash
./gradlew typecheck
./gradlew test
```
Focused checks while iterating:
```bash
./gradlew :shared:test --tests '*ContextSettingsStateTest'
./gradlew :frontend:test --tests '*ContextSettingsUiTest'
./gradlew :backend:test --tests '*KiloCliDataParserTest'
./gradlew :backend:test --tests '*KiloBackendAppServiceTest'
```
If the exact Gradle module test selectors differ, run the package-level `./gradlew test` before marking the implementation ready.
Manual verification:
1. Run `./gradlew runIde` from `packages/kilo-jetbrains/`.
2. Open `Settings -> Tools -> Kilo Code -> Context`.
3. Toggle auto-compaction and prune.
4. Set threshold to a number, apply, reopen settings, and verify it persists.
5. Clear threshold, apply, reopen settings, and verify it resets.
6. Add and remove watcher ignore patterns, apply, reopen settings, and verify the list persists.
7. Inspect the global Kilo config file through the existing `Open: global ...` action if needed.
## Risks And Follow-ups
- Global vs project-local config: this plan uses the existing global config write path. Project-local Context settings would need new workspace config RPC plumbing.
- Threshold null semantics: implement explicit clear handling; otherwise clearing the field will silently do nothing.
- String-array UI: reuse list primitives even if it takes a small adapter type; avoid one-off list widgets.
- VS Code memory/indexing parity: defer because it is not pure config and is excluded by the easy-win criteria.
- Checkpoints page: implement `snapshot` separately unless product asks to combine it with Context.
- Changeset: when implementing this user-facing JetBrains settings feature, add a patch changeset for `kilo-code`/JetBrains according to repo release guidance.
+32 -34
View File
@@ -32,7 +32,7 @@
}, },
"packages/core": { "packages/core": {
"name": "@opencode-ai/core", "name": "@opencode-ai/core",
"version": "7.4.11", "version": "7.4.13",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -127,7 +127,7 @@
}, },
"packages/effect-drizzle-sqlite": { "packages/effect-drizzle-sqlite": {
"name": "@opencode-ai/effect-drizzle-sqlite", "name": "@opencode-ai/effect-drizzle-sqlite",
"version": "7.4.11", "version": "7.4.13",
"dependencies": { "dependencies": {
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",
"effect": "catalog:", "effect": "catalog:",
@@ -141,7 +141,7 @@
}, },
"packages/effect-sqlite-node": { "packages/effect-sqlite-node": {
"name": "@opencode-ai/effect-sqlite-node", "name": "@opencode-ai/effect-sqlite-node",
"version": "7.4.11", "version": "7.4.13",
"dependencies": { "dependencies": {
"effect": "catalog:", "effect": "catalog:",
}, },
@@ -153,7 +153,7 @@
}, },
"packages/http-recorder": { "packages/http-recorder": {
"name": "@opencode-ai/http-recorder", "name": "@opencode-ai/http-recorder",
"version": "7.4.11", "version": "7.4.13",
"dependencies": { "dependencies": {
"@effect/platform-node": "4.0.0-beta.74", "@effect/platform-node": "4.0.0-beta.74",
"@effect/platform-node-shared": "4.0.0-beta.74", "@effect/platform-node-shared": "4.0.0-beta.74",
@@ -174,7 +174,7 @@
}, },
"packages/kilo-console": { "packages/kilo-console": {
"name": "@kilocode/kilo-console", "name": "@kilocode/kilo-console",
"version": "7.4.11", "version": "7.4.13",
"dependencies": { "dependencies": {
"@kilocode/kilo-indexing": "workspace:*", "@kilocode/kilo-indexing": "workspace:*",
"@kilocode/kilo-web-ui": "workspace:*", "@kilocode/kilo-web-ui": "workspace:*",
@@ -197,7 +197,7 @@
}, },
"packages/kilo-docs": { "packages/kilo-docs": {
"name": "@kilocode/kilo-docs", "name": "@kilocode/kilo-docs",
"version": "7.4.11", "version": "7.4.13",
"dependencies": { "dependencies": {
"@docsearch/css": "^4", "@docsearch/css": "^4",
"@docsearch/js": "^4", "@docsearch/js": "^4",
@@ -227,7 +227,7 @@
}, },
"packages/kilo-gateway": { "packages/kilo-gateway": {
"name": "@kilocode/kilo-gateway", "name": "@kilocode/kilo-gateway",
"version": "7.4.11", "version": "7.4.13",
"dependencies": { "dependencies": {
"@ai-sdk/alibaba": "1.0.17", "@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/anthropic": "3.0.71", "@ai-sdk/anthropic": "3.0.71",
@@ -263,7 +263,7 @@
}, },
"packages/kilo-i18n": { "packages/kilo-i18n": {
"name": "@kilocode/kilo-i18n", "name": "@kilocode/kilo-i18n",
"version": "7.4.11", "version": "7.4.13",
"devDependencies": { "devDependencies": {
"@tsconfig/node22": "catalog:", "@tsconfig/node22": "catalog:",
"@types/bun": "catalog:", "@types/bun": "catalog:",
@@ -273,7 +273,7 @@
}, },
"packages/kilo-indexing": { "packages/kilo-indexing": {
"name": "@kilocode/kilo-indexing", "name": "@kilocode/kilo-indexing",
"version": "7.4.11", "version": "7.4.13",
"dependencies": { "dependencies": {
"@aws-sdk/client-bedrock-runtime": "3.1005.0", "@aws-sdk/client-bedrock-runtime": "3.1005.0",
"@aws-sdk/credential-provider-ini": "3.972.31", "@aws-sdk/credential-provider-ini": "3.972.31",
@@ -309,7 +309,7 @@
}, },
"packages/kilo-memory": { "packages/kilo-memory": {
"name": "@kilocode/kilo-memory", "name": "@kilocode/kilo-memory",
"version": "7.4.11", "version": "7.4.13",
"dependencies": { "dependencies": {
"effect": "catalog:", "effect": "catalog:",
"zod": "catalog:", "zod": "catalog:",
@@ -323,7 +323,7 @@
}, },
"packages/kilo-sandbox": { "packages/kilo-sandbox": {
"name": "@kilocode/sandbox", "name": "@kilocode/sandbox",
"version": "7.4.11", "version": "7.4.13",
"dependencies": { "dependencies": {
"@anthropic-ai/sandbox-runtime": "catalog:", "@anthropic-ai/sandbox-runtime": "catalog:",
"effect": "catalog:", "effect": "catalog:",
@@ -338,7 +338,7 @@
}, },
"packages/kilo-telemetry": { "packages/kilo-telemetry": {
"name": "@kilocode/kilo-telemetry", "name": "@kilocode/kilo-telemetry",
"version": "7.4.11", "version": "7.4.13",
"dependencies": { "dependencies": {
"@kilocode/kilo-gateway": "workspace:*", "@kilocode/kilo-gateway": "workspace:*",
"posthog-node": "4.4.0", "posthog-node": "4.4.0",
@@ -352,7 +352,7 @@
}, },
"packages/kilo-ui": { "packages/kilo-ui": {
"name": "@kilocode/kilo-ui", "name": "@kilocode/kilo-ui",
"version": "7.4.11", "version": "7.4.13",
"dependencies": { "dependencies": {
"@kilocode/sdk": "workspace:*", "@kilocode/sdk": "workspace:*",
"@kobalte/core": "0.13.11", "@kobalte/core": "0.13.11",
@@ -389,7 +389,7 @@
}, },
"packages/kilo-vscode": { "packages/kilo-vscode": {
"name": "kilo-code", "name": "kilo-code",
"version": "7.4.11", "version": "7.4.13",
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.39.0", "@anthropic-ai/sdk": "^0.39.0",
"@kilocode/kilo-gateway": "workspace:*", "@kilocode/kilo-gateway": "workspace:*",
@@ -458,7 +458,7 @@
}, },
"packages/kilo-web-ui": { "packages/kilo-web-ui": {
"name": "@kilocode/kilo-web-ui", "name": "@kilocode/kilo-web-ui",
"version": "7.4.11", "version": "7.4.13",
"dependencies": { "dependencies": {
"@kilocode/kilo-ui": "workspace:*", "@kilocode/kilo-ui": "workspace:*",
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
@@ -475,7 +475,7 @@
}, },
"packages/llm": { "packages/llm": {
"name": "@opencode-ai/llm", "name": "@opencode-ai/llm",
"version": "7.4.11", "version": "7.4.13",
"dependencies": { "dependencies": {
"@smithy/eventstream-codec": "4.2.14", "@smithy/eventstream-codec": "4.2.14",
"@smithy/util-utf8": "4.2.2", "@smithy/util-utf8": "4.2.2",
@@ -493,7 +493,7 @@
}, },
"packages/opencode": { "packages/opencode": {
"name": "@kilocode/cli", "name": "@kilocode/cli",
"version": "7.4.11", "version": "7.4.13",
"bin": { "bin": {
"kilo": "./bin/kilo", "kilo": "./bin/kilo",
"kilocode": "./bin/kilo", "kilocode": "./bin/kilo",
@@ -660,7 +660,7 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@kilocode/plugin", "name": "@kilocode/plugin",
"version": "7.4.11", "version": "7.4.13",
"dependencies": { "dependencies": {
"@kilocode/sdk": "workspace:*", "@kilocode/sdk": "workspace:*",
"effect": "catalog:", "effect": "catalog:",
@@ -688,7 +688,7 @@
}, },
"packages/plugin-atomic-chat": { "packages/plugin-atomic-chat": {
"name": "@kilocode/plugin-atomic-chat", "name": "@kilocode/plugin-atomic-chat",
"version": "7.4.11", "version": "7.4.13",
"dependencies": { "dependencies": {
"@kilocode/plugin": "workspace:*", "@kilocode/plugin": "workspace:*",
}, },
@@ -702,7 +702,7 @@
}, },
"packages/script": { "packages/script": {
"name": "@opencode-ai/script", "name": "@opencode-ai/script",
"version": "7.4.11", "version": "7.4.13",
"dependencies": { "dependencies": {
"semver": "^7.6.3", "semver": "^7.6.3",
}, },
@@ -713,7 +713,7 @@
}, },
"packages/sdk/js": { "packages/sdk/js": {
"name": "@kilocode/sdk", "name": "@kilocode/sdk",
"version": "7.4.11", "version": "7.4.13",
"dependencies": { "dependencies": {
"cross-spawn": "catalog:", "cross-spawn": "catalog:",
}, },
@@ -728,7 +728,7 @@
}, },
"packages/server": { "packages/server": {
"name": "@opencode-ai/server", "name": "@opencode-ai/server",
"version": "7.4.11", "version": "7.4.13",
"dependencies": { "dependencies": {
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",
@@ -742,7 +742,7 @@
}, },
"packages/storybook": { "packages/storybook": {
"name": "@opencode-ai/storybook", "name": "@opencode-ai/storybook",
"version": "7.4.11", "version": "7.4.13",
"devDependencies": { "devDependencies": {
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
"@solidjs/meta": "catalog:", "@solidjs/meta": "catalog:",
@@ -765,7 +765,7 @@
}, },
"packages/tui": { "packages/tui": {
"name": "@opencode-ai/tui", "name": "@opencode-ai/tui",
"version": "7.4.11", "version": "7.4.13",
"dependencies": { "dependencies": {
"@kilocode/plugin": "workspace:*", "@kilocode/plugin": "workspace:*",
"@kilocode/sdk": "workspace:*", "@kilocode/sdk": "workspace:*",
@@ -792,7 +792,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "7.4.11", "version": "7.4.13",
"dependencies": { "dependencies": {
"@kilocode/sdk": "workspace:*", "@kilocode/sdk": "workspace:*",
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
@@ -844,23 +844,21 @@
}, },
}, },
"trustedDependencies": [ "trustedDependencies": [
"tree-sitter-powershell",
"web-tree-sitter",
"tree-sitter-bash",
"esbuild", "esbuild",
"protobufjs", "protobufjs",
"web-tree-sitter",
"tree-sitter-bash",
], ],
"patchedDependencies": { "patchedDependencies": {
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
"virtua@0.49.1": "patches/virtua@0.49.1.patch",
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch", "mammoth@1.12.0": "patches/mammoth@1.12.0.patch",
"@ai-sdk/xai@3.0.92": "patches/@ai-sdk%2Fxai@3.0.92.patch", "@ai-sdk/xai@3.0.92": "patches/@ai-sdk%2Fxai@3.0.92.patch",
"@ff-labs/fff-bun@0.9.4": "patches/@ff-labs%2Ffff-bun@0.9.4.patch", "virtua@0.49.1": "patches/virtua@0.49.1.patch",
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
"@ff-labs/fff-bun@0.9.4": "patches/@ff-labs%2Ffff-bun@0.9.4.patch",
"pacote@21.5.1": "patches/pacote@21.5.1.patch", "pacote@21.5.1": "patches/pacote@21.5.1.patch",
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
}, },
"overrides": { "overrides": {
"@effect/platform-node-shared": "4.0.0-beta.74", "@effect/platform-node-shared": "4.0.0-beta.74",
+87
View File
@@ -0,0 +1,87 @@
# JetBrains ↔ VS Code Settings Parity: Easy Wins
## How parity works here
Both clients edit the **same shared `kilo.json`** through the CLI. So any setting whose
behavior lives entirely in the CLI is an "easy win" for JetBrains: the CLI already does the
work, JetBrains just needs a UI row that writes the config key. No CLI changes, no new feature.
Structural gap: today JetBrains only has **Models / Providers / Agent Behavior / Profile**
settings pages. There is **no General / Display / Experimental / Context / Checkpoints** page.
The lift for most easy wins is:
1. Add a new `Configurable` page (using existing `settings/base/` primitives —
`BaseSettingsUi`, `SettingsRow`, `SettingsToggle`, `SettingsListPanel`), register it in
`kilo.jetbrains.frontend.xml`.
2. Extend the `buildConfigPatch` allowlist in `KiloCliDataParser.kt` (currently only
`model`, `small_model`, `subagent_model`, `subagent_variant`, `default_agent`) and add
boolean/number JSON serialization — it currently only emits strings.
3. Add localized labels to `KiloBundle.properties`.
No CLI/SDK change and no new runtime feature.
## Excluded from "easy"
| Excluded | Reason |
|---|---|
| Agent Behavior, Auto-Approve | Skipped by request |
| Indexing, Sandboxing | Imply enabling new features |
| Browser Automation | Playwright feature not present in JetBrains |
| Autocomplete (provider/model/toggles) | No autocomplete feature (flags exist only as migration stubs) |
| Agent Manager (auto-branch, prefix) | VS Code-only feature |
| Notification/attention sounds | Client must implement sound playback |
| `maxCost` alert | Client must render the alert UI |
| Commit message (`commit_message.prompt`, `languageCommitMessage`) | No commit-message generation feature in JetBrains |
| `language`, `fontSize`, `diff.renderMarkdown`, `agentWorkStyle` | VS Code-webview/onboarding-specific |
## Tier 1 — Genuinely easy (CLI does all the work; just add UI + config key)
| Setting | Config key | Type | Suggested page |
|---|---|---|---|
| Hide prompt-training models | `hide_prompt_training_models` | bool | Models |
| Enable checkpoints | `snapshot` | bool | new "Checkpoints" |
| Auto-compaction | `compaction.auto` | bool | new "Context" |
| Compaction threshold % | `compaction.threshold_percent` | number | Context |
| Prune on compaction | `compaction.prune` | bool | Context |
| Watcher ignore patterns | `watcher.ignore` | string[] | Context (list editor) |
| Display username | `username` | string | new "Display/General" |
| Share mode | `share` | enum (manual/auto/disabled) | new "Experimental" |
| Remote control on startup | `remote_control` | bool | Experimental |
| Formatter integration | `formatter` | bool | Experimental |
| LSP integration | `lsp` | bool | Experimental |
| Batch tool | `experimental.batch_tool` | bool | Experimental |
| Native notebook tools | `experimental.native_notebook_tools` | bool | Experimental |
| Continue loop on deny | `experimental.continue_loop_on_deny` | bool | Experimental |
| SWE pruner (+ model) | `experimental.swe_pruner`, `..._model` | bool + string | Experimental |
| MCP timeout | `experimental.mcp_timeout` | number | Experimental |
| Per-tool toggles | `tools.<name>` | bool | Experimental |
**Claude Code compatibility**: lives under "Agent Behavior" in VS Code, but in JetBrains the
entire backend (`KiloClaudeCompatSettings` + RPC getter/setter + spawn-env wiring) already
exists with no UI. Exposing it is the single lowest-effort item — just a checkbox bound to the
existing RPC, no config plumbing.
⚠️ Hold back `experimental.codebase_search` (leans on indexing) and
`experimental.image_generation` (adds a tool) — arguably "enabling a feature."
## Tier 2 — Config is easy, but honoring it needs JetBrains rendering work
| Setting | Config key | Extra work |
|---|---|---|
| Auto-collapse reasoning | `auto_collapse_reasoning` | Reasoning-card default collapse |
| Terminal command display | `terminal_command_display` (expanded/collapsed) | Tool-card default state |
| Code edit display | `code_edit_display` (expanded/collapsed) | Edit-card default state |
## Recommendation
Add a new **"General/Display" page + "Experimental" page** driven entirely by CLI config,
seeded with Tier 1 behavioral settings, plus wire the already-built **Claude Code compat**
toggle. This closes most of the non-feature gap with:
- zero CLI/SDK changes,
- one allowlist extension in `KiloCliDataParser.buildConfigPatch` (add keys + boolean/number serialization),
- reuse of existing `settings/base/` UI primitives and test patterns (`FakeAppRpcApi`
frontend test + `MockCliServer` backend body assertion).
Do Tier 2 (reasoning/terminal/edit display defaults) after Tier 1, since it touches the
session-rendering layer rather than being pure config.
+1 -1
View File
@@ -165,6 +165,6 @@
"pacote@21.5.1": "patches/pacote@21.5.1.patch", "pacote@21.5.1": "patches/pacote@21.5.1.patch",
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch" "mammoth@1.12.0": "patches/mammoth@1.12.0.patch"
}, },
"version": "7.4.11", "version": "7.4.13",
"peerDependencies": {} "peerDependencies": {}
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "7.4.11", "version": "7.4.13",
"name": "@opencode-ai/core", "name": "@opencode-ai/core",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+1
View File
@@ -555,6 +555,7 @@ const SessionRevert = Schema.Struct({
partID: optionalOmitUndefined(PartID), partID: optionalOmitUndefined(PartID),
snapshot: optionalOmitUndefined(Schema.String), snapshot: optionalOmitUndefined(Schema.String),
diff: optionalOmitUndefined(Schema.String), diff: optionalOmitUndefined(Schema.String),
workspace: optionalOmitUndefined(Schema.Literals(["restored", "snapshots-disabled", "unavailable"])), // kilocode_change
}) })
const SessionModel = Schema.Struct({ const SessionModel = Schema.Struct({
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "7.4.11", "version": "7.4.13",
"name": "@opencode-ai/effect-drizzle-sqlite", "name": "@opencode-ai/effect-drizzle-sqlite",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "7.4.11", "version": "7.4.13",
"name": "@opencode-ai/effect-sqlite-node", "name": "@opencode-ai/effect-sqlite-node",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+6 -6
View File
@@ -1,7 +1,7 @@
id = "kilo" id = "kilo"
name = "Kilo" name = "Kilo"
description = "The open source coding agent." description = "The open source coding agent."
version = "7.4.11" version = "7.4.13"
schema_version = 1 schema_version = 1
authors = ["Anomaly"] authors = ["Anomaly"]
repository = "https://github.com/Kilo-Org/kilocode" repository = "https://github.com/Kilo-Org/kilocode"
@@ -11,26 +11,26 @@ name = "Kilo"
icon = "./icons/opencode.svg" icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64] [agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.11/opencode-darwin-arm64.zip" archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.13/opencode-darwin-arm64.zip"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64] [agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.11/opencode-darwin-x64.zip" archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.13/opencode-darwin-x64.zip"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64] [agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.11/opencode-linux-arm64.tar.gz" archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.13/opencode-linux-arm64.tar.gz"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64] [agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.11/opencode-linux-x64.tar.gz" archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.13/opencode-linux-x64.tar.gz"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64] [agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.11/opencode-windows-x64.zip" archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.13/opencode-windows-x64.zip"
cmd = "./opencode.exe" cmd = "./opencode.exe"
args = ["acp"] args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "7.4.11", "version": "7.4.13",
"name": "@opencode-ai/http-recorder", "name": "@opencode-ai/http-recorder",
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes", "description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@kilocode/kilo-console", "name": "@kilocode/kilo-console",
"version": "7.4.11", "version": "7.4.13",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@kilocode/kilo-docs", "name": "@kilocode/kilo-docs",
"version": "7.4.11", "version": "7.4.13",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --webpack --port 3002", "dev": "next dev --webpack --port 3002",
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6a89d1a9a31f7de1780d772b7caf71d96f0b4b895df954c77dcd508a7febe7c9
size 1950
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:431a0c12cb844b5d4e535cc4c7bf5546fd9e66a178b8023b00978d07c291a531
size 3919
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:42c43e9ba12bc73da98c915dafca274beee1d0c893c168d8af4e3e3f41edd367 oid sha256:3b1eb6cf15d5c2dc66c769a91fcf09bcf25bc2e8d17696ad0e9ab4181ed3038c
size 630896 size 658647
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-gateway", "name": "@kilocode/kilo-gateway",
"version": "7.4.11", "version": "7.4.13",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration", "description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
-5
View File
@@ -97,11 +97,6 @@ export async function fetchKiloModels(options?: {
const models: Record<string, any> = {} const models: Record<string, any> = {}
for (const model of raw.data) { for (const model of raw.data) {
// Skip image generation models
if (model.architecture?.output_modalities?.includes("image")) {
continue
}
// Skip models that don't support tools — Kilo requires tool calling // Skip models that don't support tools — Kilo requires tool calling
if (!model.supported_parameters?.includes("tools")) { if (!model.supported_parameters?.includes("tools")) {
continue continue
@@ -248,3 +248,85 @@ test("returns error with kind=schema when response body is invalid JSON", async
expect(result.models).toEqual({}) expect(result.models).toEqual({})
expect(result.error?.kind).toBe("schema") expect(result.error?.kind).toBe("schema")
}) })
const MIXED_MODALITY_RESPONSE = JSON.stringify({
data: [
{
id: "openrouter/auto",
name: "Auto Router",
context_length: 2000000,
max_completion_tokens: 16384,
architecture: {
input_modalities: ["text", "image"],
output_modalities: ["text", "image"],
},
supported_parameters: ["tools", "temperature"],
},
{
id: "openrouter/auto-beta",
name: "Auto Router (Beta)",
context_length: 2000000,
max_completion_tokens: 16384,
architecture: {
input_modalities: ["text", "image"],
output_modalities: ["text", "image"],
},
supported_parameters: ["tools", "temperature"],
},
{
id: "black-forest-labs/flux-1.1-pro",
name: "FLUX 1.1 Pro",
context_length: 4096,
max_completion_tokens: 4096,
architecture: {
input_modalities: ["text", "image"],
output_modalities: ["image"],
},
supported_parameters: ["tools"],
},
{
id: "test/no-tools",
name: "No Tools Model",
context_length: 128000,
max_completion_tokens: 16384,
architecture: {
input_modalities: ["text"],
output_modalities: ["text"],
},
supported_parameters: ["temperature"],
},
{
id: "test/model-a",
name: "Test Model A",
context_length: 128000,
max_completion_tokens: 16384,
architecture: {
input_modalities: ["text"],
output_modalities: ["text"],
},
supported_parameters: ["tools", "temperature"],
},
],
})
test("keeps image-output models with tools and drops models without tools", async () => {
const orig = globalThis.fetch
stubFetch(
async () =>
new Response(MIXED_MODALITY_RESPONSE, {
status: 200,
headers: { "content-type": "application/json" },
}),
)
const result = await fetchKiloModels({})
;(globalThis as any).fetch = orig
expect(result.error).toBeUndefined()
expect(result.models["openrouter/auto"]).toBeDefined()
expect(result.models["openrouter/auto-beta"]).toBeDefined()
expect(result.models["black-forest-labs/flux-1.1-pro"]).toBeDefined()
expect(result.models["test/model-a"]).toBeDefined()
expect(result.models["test/no-tools"]).toBeUndefined()
})
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-i18n", "name": "@kilocode/kilo-i18n",
"version": "7.4.11", "version": "7.4.13",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"description": "Kilo-specific i18n translations and overrides", "description": "Kilo-specific i18n translations and overrides",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-indexing", "name": "@kilocode/kilo-indexing",
"version": "7.4.11", "version": "7.4.13",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"description": "Standalone indexing engine and host helpers for Kilo Code", "description": "Standalone indexing engine and host helpers for Kilo Code",
+6
View File
@@ -1,5 +1,11 @@
# Changelog # Changelog
## 7.4.12
### Patch Changes
- [#12191](https://github.com/Kilo-Org/kilocode/pull/12191) [`4d676b6`](https://github.com/Kilo-Org/kilocode/commit/4d676b68d2d0dd025c7d1a6684f49f3d03e9d12d) - Use Kilo Core for JetBrains @ file completion.
## 7.4.10 ## 7.4.10
### Patch Changes ### Patch Changes
@@ -820,11 +820,13 @@ class KiloBackendAppService private constructor(
} }
} }
"global.disposed" -> { "global.disposed" -> {
logSessionDisposalRisk("global.disposed")
log.info("SSE global.disposed — triggering full application reload") log.info("SSE global.disposed — triggering full application reload")
val current = _appState.value val current = _appState.value
if (current is KiloAppState.Ready) load() if (current is KiloAppState.Ready) load()
} }
"server.instance.disposed" -> { "server.instance.disposed" -> {
logSessionDisposalRisk("server.instance.disposed")
log.info("SSE server.instance.disposed — triggering full application reload") log.info("SSE server.instance.disposed — triggering full application reload")
val current = _appState.value val current = _appState.value
if (current is KiloAppState.Ready) load() 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() { private suspend fun clear() {
synchronized(loadLock) { synchronized(loadLock) {
val jobs = listOfNotNull(loader, eventWatcher) val jobs = listOfNotNull(loader, eventWatcher)
@@ -86,6 +86,10 @@ class KiloBackendSessionManager(
} }
fun stop() { 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?.cancel()
watcher = null watcher = null
client = null client = null
@@ -20,6 +20,7 @@ import ai.kilocode.rpc.dto.CommandDto
import ai.kilocode.rpc.dto.ConfigDto import ai.kilocode.rpc.dto.ConfigDto
import ai.kilocode.rpc.dto.ConfigPatchDto import ai.kilocode.rpc.dto.ConfigPatchDto
import ai.kilocode.rpc.dto.ConfigUpdateDto import ai.kilocode.rpc.dto.ConfigUpdateDto
import ai.kilocode.rpc.dto.CompactionConfigDto
import ai.kilocode.rpc.dto.CustomModelDto import ai.kilocode.rpc.dto.CustomModelDto
import ai.kilocode.rpc.dto.CustomProviderConfigDto import ai.kilocode.rpc.dto.CustomProviderConfigDto
import ai.kilocode.rpc.dto.CustomProviderSaveDto import ai.kilocode.rpc.dto.CustomProviderSaveDto
@@ -73,6 +74,7 @@ import ai.kilocode.rpc.dto.TodoDto
import ai.kilocode.rpc.dto.TodoViewDto import ai.kilocode.rpc.dto.TodoViewDto
import ai.kilocode.rpc.dto.TokensDto import ai.kilocode.rpc.dto.TokensDto
import ai.kilocode.rpc.dto.ToolRefDto import ai.kilocode.rpc.dto.ToolRefDto
import ai.kilocode.rpc.dto.WatcherConfigDto
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
@@ -521,6 +523,8 @@ object KiloCliDataParser {
subagentModel = obj.str("subagent_model"), subagentModel = obj.str("subagent_model"),
subagentVariant = obj.str("subagent_variant"), subagentVariant = obj.str("subagent_variant"),
defaultAgent = obj.str("default_agent"), defaultAgent = obj.str("default_agent"),
watcher = parseWatcherConfig(obj["watcher"].obj()),
compaction = parseCompactionConfig(obj["compaction"].obj()),
instructions = obj["instructions"].arr() instructions = obj["instructions"].arr()
?.mapNotNull { runCatching { it.jsonPrimitive.contentOrNull }.getOrNull() } ?.mapNotNull { runCatching { it.jsonPrimitive.contentOrNull }.getOrNull() }
?: emptyList(), ?: emptyList(),
@@ -530,6 +534,24 @@ object KiloCliDataParser {
) )
}.getOrDefault(ConfigDto()) }.getOrDefault(ConfigDto())
private fun parseWatcherConfig(obj: JsonObject?): WatcherConfigDto? {
if (obj == null) return null
return WatcherConfigDto(
ignore = obj["ignore"].arr()
?.mapNotNull { runCatching { it.jsonPrimitive.contentOrNull }.getOrNull() }
?: emptyList(),
)
}
private fun parseCompactionConfig(obj: JsonObject?): CompactionConfigDto? {
if (obj == null) return null
return CompactionConfigDto(
auto = runCatching { obj.flagOrNull("auto") }.getOrNull(),
threshold_percent = runCatching { obj.num("threshold_percent") }.getOrNull(),
prune = runCatching { obj.flagOrNull("prune") }.getOrNull(),
)
}
private fun parseSkillsConfig(obj: JsonObject?): SkillsConfigDto? { private fun parseSkillsConfig(obj: JsonObject?): SkillsConfigDto? {
if (obj == null) return null if (obj == null) return null
return SkillsConfigDto( return SkillsConfigDto(
@@ -625,7 +647,12 @@ object KiloCliDataParser {
val obj = item.obj() ?: return@mapNotNull null val obj = item.obj() ?: return@mapNotNull null
val name = obj.str("name") ?: return@mapNotNull null val name = obj.str("name") ?: return@mapNotNull null
val location = obj.str("location") ?: 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> = fun parseAgentBehaviorCommands(raw: String): List<CommandDto> =
@@ -838,6 +865,24 @@ object KiloCliDataParser {
val instructions = patch.instructions val instructions = patch.instructions
if (instructions != null) put("instructions", JsonArray(instructions.map(::JsonPrimitive))) if (instructions != null) put("instructions", JsonArray(instructions.map(::JsonPrimitive)))
val watcher = patch.watcher
if (watcher != null) {
put("watcher", buildJsonObject {
val ignore = watcher.ignore
if (ignore != null) put("ignore", JsonArray(ignore.map(::JsonPrimitive)))
})
}
val compaction = patch.compaction
if (compaction != null) {
put("compaction", buildJsonObject {
for (field in compaction.clear) put(field, JsonNull)
if (compaction.auto != null) put("auto", compaction.auto)
if (compaction.threshold_percent != null) put("threshold_percent", compaction.threshold_percent)
if (compaction.prune != null) put("prune", compaction.prune)
})
}
val skills = patch.skills val skills = patch.skills
if (skills != null) { if (skills != null) {
put("skills", buildJsonObject { put("skills", buildJsonObject {
@@ -14,6 +14,7 @@ import ai.kilocode.rpc.dto.ConfigPatchDto
import ai.kilocode.rpc.dto.McpConfigDto import ai.kilocode.rpc.dto.McpConfigDto
import ai.kilocode.rpc.dto.McpServerConfigDto import ai.kilocode.rpc.dto.McpServerConfigDto
import ai.kilocode.rpc.dto.PermissionRuleItemDto import ai.kilocode.rpc.dto.PermissionRuleItemDto
import ai.kilocode.rpc.dto.SkillDto
import com.intellij.openapi.components.service import com.intellij.openapi.components.service
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -22,7 +23,11 @@ import kotlinx.serialization.json.JsonPrimitive
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import com.intellij.openapi.util.SystemInfo
import java.net.URLEncoder 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.nio.charset.StandardCharsets
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
@@ -33,6 +38,7 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? =
private val JSON = "application/json".toMediaType() private val JSON = "application/json".toMediaType()
private val saved = ConcurrentHashMap<String, SavedMcp>() private val saved = ConcurrentHashMap<String, SavedMcp>()
private val port = AtomicInteger(-1) private val port = AtomicInteger(-1)
private val extensions = setOf("md", "markdown", "txt", "text", "html", "htm")
} }
private val app: KiloBackendAppService get() = backend ?: service() 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 = override suspend fun removeSkill(directory: String, location: String): Boolean =
post(directory, "/kilocode/skill/remove", JsonObject(mapOf("location" to JsonPrimitive(location)))) 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 = override suspend fun removeAgent(directory: String, name: String): Boolean =
post(directory, "/kilocode/agent/remove", JsonObject(mapOf("name" to JsonPrimitive(name)))) post(directory, "/kilocode/agent/remove", JsonObject(mapOf("name" to JsonPrimitive(name))))
@@ -139,6 +193,104 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? =
return true 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) { private suspend fun patchConfig(path: String, body: String): Unit = withContext(Dispatchers.IO) {
val http = app.http ?: throw IllegalStateException("Kilo HTTP client is unavailable") val http = app.http ?: throw IllegalStateException("Kilo HTTP client is unavailable")
val url = "http://127.0.0.1:${app.port}$path" 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 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( private data class SavedMcp(
val directory: String, val directory: String,
val name: String, val name: String,
@@ -64,6 +64,8 @@ internal object KiloWorkspaceDtoMapper {
name = s.name, name = s.name,
description = s.description, description = s.description,
location = s.location, location = s.location,
content = s.content,
editable = false,
) )
private fun provider(p: ProviderInfo) = ProviderDto( private fun provider(p: ProviderInfo) = ProviderDto(
@@ -22,33 +22,22 @@ import ai.kilocode.rpc.dto.ModelsWorkspaceDto
import ai.kilocode.rpc.dto.WorkspaceFileDto import ai.kilocode.rpc.dto.WorkspaceFileDto
import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.CapturingProcessHandler import com.intellij.execution.process.CapturingProcessHandler
import com.intellij.ide.actions.searcheverywhere.FoundItemDescriptor
import com.intellij.ide.util.gotoByName.ChooseByNameInScopeItemProvider
import com.intellij.ide.util.gotoByName.ChooseByNamePopup
import com.intellij.ide.util.gotoByName.ChooseByNameViewModel
import com.intellij.ide.util.gotoByName.GotoFileModel
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.readAction
import com.intellij.openapi.components.service import com.intellij.openapi.components.service
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.IndexNotReadyException
import com.intellij.openapi.fileEditor.OpenFileDescriptor import com.intellij.openapi.fileEditor.OpenFileDescriptor
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.platform.project.ProjectId import com.intellij.platform.project.ProjectId
import com.intellij.platform.project.findProjectOrNull import com.intellij.platform.project.findProjectOrNull
import com.intellij.navigation.NavigationItem import kotlinx.coroutines.CancellationException
import com.intellij.psi.PsiFileSystemItem
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.indexing.FindSymbolParameters
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
@@ -56,6 +45,8 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Request import okhttp3.Request
import java.net.URI import java.net.URI
import java.net.URLDecoder import java.net.URLDecoder
@@ -74,7 +65,9 @@ import kotlin.coroutines.resume
* for the given directory. Project lookup is only used to resolve the * for the given directory. Project lookup is only used to resolve the
* calling frontend project to the correct backend directory. * calling frontend project to the correct backend directory.
*/ */
class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi { class KiloWorkspaceRpcApiImpl internal constructor(
private val svc: KiloBackendAppService? = null,
) : KiloWorkspaceRpcApi {
companion object { companion object {
private val LOG = KiloLog.create(KiloWorkspaceRpcApiImpl::class.java) private val LOG = KiloLog.create(KiloWorkspaceRpcApiImpl::class.java)
private const val SCHEMA = "https://app.kilo.ai/config.json" private const val SCHEMA = "https://app.kilo.ai/config.json"
@@ -82,15 +75,15 @@ class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi {
private val LEGACY = listOf("opencode.jsonc", "opencode.json") private val LEGACY = listOf("opencode.jsonc", "opencode.json")
private val GLOBAL = MODERN + LEGACY + "config.json" private val GLOBAL = MODERN + LEGACY + "config.json"
private val LOCAL_DIRS = listOf(".kilo", ".kilocode", ".opencode") private val LOCAL_DIRS = listOf(".kilo", ".kilocode", ".opencode")
private const val SEARCH_CAP = 2_000
private const val DIFF_CAP = 200_000 private const val DIFF_CAP = 200_000
private val JSON = Json { ignoreUnknownKeys = true }
private val CONFIG = """{ private val CONFIG = """{
"${'$'}schema": "$SCHEMA" "${'$'}schema": "$SCHEMA"
} }
""" """
} }
private val app: KiloBackendAppService get() = service() private val app: KiloBackendAppService get() = svc ?: service()
private val gitCache = ConcurrentHashMap<String, Boolean>() private val gitCache = ConcurrentHashMap<String, Boolean>()
@@ -196,19 +189,56 @@ class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi {
override suspend fun searchFiles(directory: String, query: String, limit: Int): FileSearchResultDto { override suspend fun searchFiles(directory: String, query: String, limit: Int): FileSearchResultDto {
val base = file(clean(directory) ?: directory) ?: return FileSearchResultDto() val base = file(clean(directory) ?: directory) ?: return FileSearchResultDto()
val git = withContext(Dispatchers.IO) { gitAvailable(base) } val git = withContext(Dispatchers.IO) { gitAvailable(base) }
val project = project(base) ?: return FileSearchResultDto(git = git) LOG.debug { "workspace file search directory=$directory query=$query limit=$limit" }
if (DumbService.getInstance(project).isDumb) return FileSearchResultDto(indexing = true, git = git) return searchKilo(directory, query, limit, git)
}
private suspend fun searchKilo(directory: String, query: String, limit: Int, git: Boolean): FileSearchResultDto {
return try { return try {
val files = readAction { search(project, base, query, limit.coerceIn(1, 200)) } val cap = limit.coerceIn(1, 200)
FileSearchResultDto(files = files, git = git) val (files, dirs) = coroutineScope {
} catch (e: IndexNotReadyException) { val files = async { kiloResults(directory, query, "file", cap, false) }
FileSearchResultDto(indexing = true, git = git) val dirs = async { kiloResults(directory, query, "directory", cap, true) }
} catch (e: LinkageError) { files.await() to dirs.await()
LOG.warn("file search API unavailable; returning no suggestions", e) }
val found = linkedMapOf<String, WorkspaceFileDto>()
(dirs + files).forEach { file -> found.putIfAbsent(file.path, file) }
FileSearchResultDto(files = found.values.take(cap), git = git)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
LOG.warn("Kilo Core file search failed for directory=$directory query=$query", e)
FileSearchResultDto(git = git) FileSearchResultDto(git = git)
} }
} }
private suspend fun kiloResults(
directory: String,
query: String,
type: String,
limit: Int,
dir: Boolean,
): List<WorkspaceFileDto> {
val http = app.http ?: throw IllegalStateException("Kilo HTTP client is unavailable")
val raw = withContext(Dispatchers.IO) {
val request = Request.Builder()
.url("http://127.0.0.1:${app.port}/find/file?directory=${encode(directory)}&query=${encode(query)}&type=$type&limit=$limit")
.get()
.build()
http.newCall(request).execute().use { response ->
val body = response.body?.string().orEmpty()
if (!response.isSuccessful) throw RuntimeException("HTTP ${response.code}: $body")
body
}
}
return JSON.decodeFromString<List<String>>(raw)
.asSequence()
.map { it.trimEnd('/') }
.filter { it.isNotBlank() && !isManagedWorktreeStorage(it) }
.map { WorkspaceFileDto(it, it.substringAfterLast('/'), dir) }
.toList()
}
override suspend fun gitChanges(directory: String): String? = withContext(Dispatchers.IO) { override suspend fun gitChanges(directory: String): String? = withContext(Dispatchers.IO) {
val base = file(clean(directory) ?: directory) ?: return@withContext null val base = file(clean(directory) ?: directory) ?: return@withContext null
if (!gitAvailable(base)) return@withContext null if (!gitAvailable(base)) return@withContext null
@@ -311,10 +341,11 @@ class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi {
} }
descriptor.navigate(true) descriptor.navigate(true)
if (cont.isActive) cont.resume(Unit) if (cont.isActive) cont.resume(Unit)
}, ModalityState.any()) }, ModalityState.nonModal())
} }
private fun project(path: Path): Project? { private fun project(path: Path): Project? {
if (ApplicationManager.getApplication() == null) return null
val projects = ProjectManager.getInstance().openProjects.filter { !it.isDefault } val projects = ProjectManager.getInstance().openProjects.filter { !it.isDefault }
return projects.firstOrNull { item -> return projects.firstOrNull { item ->
val base = item.basePath?.let(::file) ?: return@firstOrNull false val base = item.basePath?.let(::file) ?: return@firstOrNull false
@@ -322,83 +353,6 @@ class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi {
} ?: projects.firstOrNull() } ?: projects.firstOrNull()
} }
// Uses the IDE Go-to-File engine (com.intellij.ide.util.gotoByName.*). These are public but
// unstable lang-impl classes (not @ApiStatus.Internal) -- the same engine behind Search Everywhere,
// chosen for proven large-repo performance. searchFiles() degrades gracefully on LinkageError.
@Suppress("UnstableApiUsage")
private fun search(project: Project, base: Path, query: String, limit: Int): List<WorkspaceFileDto> {
val text = query.trim()
if (text.isBlank()) return roots(project, base, limit)
val scope = GlobalSearchScope.projectScope(project)
val model = object : GotoFileModel(project) {
override fun acceptItem(item: NavigationItem): Boolean {
val psi = item as? PsiFileSystemItem ?: return false
val path = file(psi.virtualFile.path) ?: return false
return relativeWithinWorkspace(base, path) != null && super.acceptItem(item)
}
override fun loadInitialCheckBoxState(): Boolean = false
override fun saveInitialCheckBoxState(state: Boolean) {}
}
val view = object : ChooseByNameViewModel {
override fun getProject(): Project = project
override fun getModel() = model
override fun isSearchInAnyPlace(): Boolean = model.useMiddleMatching()
override fun transformPattern(pattern: String): String = ChooseByNamePopup.getTransformedPattern(pattern, model)
override fun canShowListForEmptyPattern(): Boolean = false
override fun getMaximumListSizeLimit(): Int = limit
}
val provider = model.getItemProvider(null)
val params = FindSymbolParameters.wrap(text, scope)
val found = mutableListOf<FoundItemDescriptor<*>>()
val indicator = EmptyProgressIndicator()
if (provider is ChooseByNameInScopeItemProvider) {
provider.filterElementsWithWeights(view, params, indicator) { item ->
found += item
found.size < SEARCH_CAP
}
} else {
provider.filterElements(view, text, false, indicator) { item ->
found += FoundItemDescriptor(item, 0)
found.size < SEARCH_CAP
}
}
return found.asSequence()
.sortedByDescending { it.weight }
.mapNotNull { item -> (item.item as? PsiFileSystemItem)?.virtualFile }
.mapNotNull { vf -> fileDto(base, vf) }
.distinctBy { it.path }
.take(limit)
.toList()
}
private fun roots(project: Project, base: Path, limit: Int): List<WorkspaceFileDto> {
val root = LocalFileSystem.getInstance().refreshAndFindFileByNioFile(base) ?: return emptyList()
val index = ProjectFileIndex.getInstance(project)
return root.children.asSequence()
.filter { it.name != ".git" }
.filterNot { index.isExcluded(it) }
.mapNotNull { fileDto(base, it) }
.sortedWith(
compareByDescending<WorkspaceFileDto> { it.directory }
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
)
.take(limit)
.toList()
}
private fun fileDto(base: Path, vf: VirtualFile): WorkspaceFileDto? {
val path = file(vf.path) ?: return null
val rel = relativeWithinWorkspace(base, path) ?: return null
return WorkspaceFileDto(rel, vf.name, vf.isDirectory)
}
private fun gitAvailable(base: Path): Boolean { private fun gitAvailable(base: Path): Boolean {
return workspaceGitAvailable(base, gitCache) return workspaceGitAvailable(base, gitCache)
} }
@@ -254,6 +254,7 @@ class KiloBackendWorkspace(
name = s.name, name = s.name,
description = s.description, description = s.description,
location = s.location, location = s.location,
content = s.content,
) )
}) })
} catch (e: CancellationException) { } catch (e: CancellationException) {
@@ -143,4 +143,5 @@ data class SkillInfo(
val name: String, val name: String,
val description: String?, val description: String?,
val location: String, val location: String,
val content: String?,
) )
@@ -9,7 +9,9 @@ import ai.kilocode.backend.testing.FakeCliServer
import ai.kilocode.backend.testing.MockCliServer import ai.kilocode.backend.testing.MockCliServer
import ai.kilocode.backend.testing.TestLog import ai.kilocode.backend.testing.TestLog
import ai.kilocode.rpc.dto.AgentConfigPatchDto import ai.kilocode.rpc.dto.AgentConfigPatchDto
import ai.kilocode.rpc.dto.CompactionPatchDto
import ai.kilocode.rpc.dto.ConfigPatchDto import ai.kilocode.rpc.dto.ConfigPatchDto
import ai.kilocode.rpc.dto.WatcherPatchDto
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -222,6 +224,28 @@ class KiloBackendAppServiceTest {
assertEquals("fast", svc.config?.agent?.get("code")?.variant) assertEquals("fast", svc.config?.agent?.get("code")?.variant)
} }
@Test
fun `update config patches context settings and reloads`() = runBlocking {
val svc = create()
svc.connect()
ready(svc)
val state = svc.updateConfig(ConfigPatchDto(
watcher = WatcherPatchDto(ignore = listOf("**/dist/**", "tmp/**")),
compaction = CompactionPatchDto(auto = false, threshold_percent = 75.5, prune = false),
))
assertEquals(
"{\"watcher\":{\"ignore\":[\"**/dist/**\",\"tmp/**\"]},\"compaction\":{\"auto\":false,\"threshold_percent\":75.5,\"prune\":false}}",
mock.lastConfigPatchBody,
)
val cfg = appStateDto(state).config
assertEquals(listOf("**/dist/**", "tmp/**"), cfg?.watcher?.ignore)
assertEquals(false, cfg?.compaction?.auto)
assertEquals(75.5, cfg?.compaction?.threshold_percent)
assertEquals(false, svc.config?.compaction?.prune)
}
@Test @Test
fun `ready dto maps model config`() = runBlocking { fun `ready dto maps model config`() = runBlocking {
mock.config = """{"model":"openai/gpt","agent":{"plan":{"model":"anthropic/claude","variant":"high"}}}""" mock.config = """{"model":"openai/gpt","agent":{"plan":{"model":"anthropic/claude","variant":"high"}}}"""
@@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.withTimeoutOrNull
import okhttp3.Request import okhttp3.Request
import okhttp3.sse.EventSource import okhttp3.sse.EventSource
import okhttp3.sse.EventSourceListener import okhttp3.sse.EventSourceListener
@@ -36,6 +37,10 @@ import kotlin.test.assertTrue
class KiloConnectionServiceTest { class KiloConnectionServiceTest {
private companion object {
const val WAIT_MS = 15_000L
}
private val mock = MockCliServer() private val mock = MockCliServer()
private val fake = FakeCliServer(mock) private val fake = FakeCliServer(mock)
private val log = TestLog() private val log = TestLog()
@@ -101,20 +106,23 @@ class KiloConnectionServiceTest {
val svc = KiloConnectionService(scope, server, {}, log) val svc = KiloConnectionService(scope, server, {}, log)
val job = scope.launch { svc.connect() } val job = scope.launch { svc.connect() }
val downloading = withTimeout(5_000) { val downloading = withTimeout(WAIT_MS) {
svc.state.first { it is ConnectionState.Downloading } svc.state.first { it is ConnectionState.Downloading }
} }
assertEquals(ConnectionState.Downloading(42, "1.2.3", "darwin-arm64"), downloading) assertEquals(ConnectionState.Downloading(42, "1.2.3", "darwin-arm64"), downloading)
resolved.complete(Unit) resolved.complete(Unit)
withTimeout(5_000) { withTimeout(WAIT_MS) {
svc.state.first { it == ConnectionState.Connecting } svc.state.first { it == ConnectionState.Connecting }
} }
ready.complete(Unit) ready.complete(Unit)
withTimeout(5_000) { val connected = withTimeoutOrNull(WAIT_MS) {
svc.state.first { it is ConnectionState.Connected } 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() job.join()
} }
@@ -4,6 +4,7 @@ import ai.kilocode.backend.workspace.CommandInfo
import ai.kilocode.backend.workspace.ProviderData import ai.kilocode.backend.workspace.ProviderData
import ai.kilocode.rpc.dto.ChatEventDto import ai.kilocode.rpc.dto.ChatEventDto
import ai.kilocode.rpc.dto.AgentConfigPatchDto import ai.kilocode.rpc.dto.AgentConfigPatchDto
import ai.kilocode.rpc.dto.CompactionPatchDto
import ai.kilocode.rpc.dto.ConfigDto import ai.kilocode.rpc.dto.ConfigDto
import ai.kilocode.rpc.dto.ConfigPatchDto import ai.kilocode.rpc.dto.ConfigPatchDto
import ai.kilocode.rpc.dto.ConfigUpdateDto import ai.kilocode.rpc.dto.ConfigUpdateDto
@@ -19,6 +20,7 @@ import ai.kilocode.rpc.dto.PromptDto
import ai.kilocode.rpc.dto.PromptPartDto import ai.kilocode.rpc.dto.PromptPartDto
import ai.kilocode.rpc.dto.QuestionReplyDto import ai.kilocode.rpc.dto.QuestionReplyDto
import ai.kilocode.rpc.dto.SkillsPatchDto import ai.kilocode.rpc.dto.SkillsPatchDto
import ai.kilocode.rpc.dto.WatcherPatchDto
import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Nested
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@@ -1173,6 +1175,38 @@ class KiloCliDataParserTest {
assertEquals(listOf("https://example.test/skill.md"), cfg.skills?.urls) assertEquals(listOf("https://example.test/skill.md"), cfg.skills?.urls)
} }
@Test
fun `parseConfig - context settings`() {
val cfg = KiloCliDataParser.parseConfig(
"""{
"watcher":{"ignore":["**/dist/**","tmp/**"]},
"compaction":{"auto":true,"threshold_percent":75.5,"prune":false}
}"""
)
assertEquals(listOf("**/dist/**", "tmp/**"), cfg.watcher?.ignore)
assertEquals(true, cfg.compaction?.auto)
assertEquals(75.5, cfg.compaction?.threshold_percent)
assertEquals(false, cfg.compaction?.prune)
}
@Test
fun `parseConfig - malformed compaction fields do not discard config`() {
val cfg = KiloCliDataParser.parseConfig(
"""{
"model":"openai/gpt",
"watcher":{"ignore":["tmp/**"]},
"compaction":{"auto":{},"threshold_percent":[],"prune":false}
}"""
)
assertEquals("openai/gpt", cfg.model)
assertEquals(listOf("tmp/**"), cfg.watcher?.ignore)
assertNull(cfg.compaction?.auto)
assertNull(cfg.compaction?.threshold_percent)
assertEquals(false, cfg.compaction?.prune)
}
@Test @Test
fun `parseConfig - agent overrides and permissions`() { fun `parseConfig - agent overrides and permissions`() {
val cfg = KiloCliDataParser.parseConfig( val cfg = KiloCliDataParser.parseConfig(
@@ -2123,6 +2157,29 @@ class KiloCliDataParserTest {
) )
} }
@Test
fun `buildConfigPatch - context watcher and compaction fields`() {
val patch = ConfigPatchDto(
watcher = WatcherPatchDto(ignore = listOf("**/dist/**", "tmp/**")),
compaction = CompactionPatchDto(auto = false, threshold_percent = 75.5, prune = false),
)
assertEquals(
"{\"watcher\":{\"ignore\":[\"**/dist/**\",\"tmp/**\"]},\"compaction\":{\"auto\":false,\"threshold_percent\":75.5,\"prune\":false}}",
KiloCliDataParser.buildConfigPatch(patch),
)
}
@Test
fun `buildConfigPatch - context threshold clear emits null`() {
val patch = ConfigPatchDto(compaction = CompactionPatchDto(clear = listOf("threshold_percent")))
assertEquals(
"{\"compaction\":{\"threshold_percent\":null}}",
KiloCliDataParser.buildConfigPatch(patch),
)
}
@Test @Test
fun `buildConfigPatch - mcp upsert and delete`() { fun `buildConfigPatch - mcp upsert and delete`() {
val patch = ConfigPatchDto(mcp = linkedMapOf( val patch = ConfigPatchDto(mcp = linkedMapOf(
@@ -14,6 +14,8 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import java.nio.file.Files
import java.nio.file.Path
import kotlin.test.AfterTest import kotlin.test.AfterTest
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertContains import kotlin.test.assertContains
@@ -78,6 +80,156 @@ class KiloAgentBehaviorRpcApiImplTest {
assertContains(err.message.orEmpty(), "HTTP 400") 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 @Test
fun `mcp config writes global and workspace patches`() = runBlocking { fun `mcp config writes global and workspace patches`() = runBlocking {
mock.config = """{"mcp":{"global":{"type":"local","command":["node","g.js"]}}}""" mock.config = """{"mcp":{"global":{"type":"local","command":["node","g.js"]}}}"""
@@ -0,0 +1,84 @@
package ai.kilocode.backend.rpc
import ai.kilocode.backend.app.KiloAppState
import ai.kilocode.backend.app.KiloBackendAppService
import ai.kilocode.backend.testing.FakeCliServer
import ai.kilocode.backend.testing.MockCliServer
import ai.kilocode.backend.testing.TestLog
import ai.kilocode.rpc.dto.WorkspaceFileDto
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
import java.nio.file.Files
import kotlin.test.AfterTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class KiloWorkspaceRpcApiImplTest {
private val mock = MockCliServer()
private val log = TestLog()
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val apps = mutableListOf<KiloBackendAppService>()
@AfterTest
fun tearDown() = runBlocking {
apps.forEach { it.dispose() }
apps.clear()
scope.cancel()
mock.close()
}
@Test
fun `searches files and directories through core`() = runBlocking {
mock.findFiles = """["src/Main.kt",".kilo/worktrees/hidden.kt"]"""
mock.findDirectories = """["src/","docs/"]"""
val dir = Files.createTempDirectory("kilo-search")
try {
val app = app()
val result = KiloWorkspaceRpcApiImpl(app).searchFiles(dir.toString(), "src", 3)
assertEquals(
listOf(
WorkspaceFileDto("src", "src", directory = true),
WorkspaceFileDto("docs", "docs", directory = true),
WorkspaceFileDto("src/Main.kt", "Main.kt"),
),
result.files,
)
assertEquals(2, mock.requestCount("/find/file"))
assertTrue(mock.findFilePaths.any { it.contains("type=file") && it.contains("query=src") })
assertTrue(mock.findFilePaths.any { it.contains("type=directory") && it.contains("query=src") })
} finally {
delete(dir)
}
}
private suspend fun app(): KiloBackendAppService {
val app = KiloBackendAppService.create(scope, FakeCliServer(mock), log).also { apps.add(it) }
app.connect()
val state = assertNotNull(
withTimeoutOrNull(35_000) {
app.appState.first {
it is KiloAppState.Ready || it is KiloAppState.Error || it is KiloAppState.MigrationRequired
}
},
"App startup timed out in ${app.appState.value}; logs=${log.messages}",
)
assertIs<KiloAppState.Ready>(state, "App startup failed; logs=${log.messages}")
return app
}
private fun delete(dir: java.nio.file.Path) {
Files.walk(dir).use { paths ->
paths.sorted(Comparator.reverseOrder()).forEach { Files.deleteIfExists(it) }
}
}
}
@@ -11,6 +11,7 @@ import java.net.ServerSocket
import java.net.Socket import java.net.Socket
import java.net.SocketException import java.net.SocketException
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -67,9 +68,11 @@ class MockCliServer : AutoCloseable {
@Volatile var mcpStatus = 200 @Volatile var mcpStatus = 200
@Volatile var mcpActionStatus = 200 @Volatile var mcpActionStatus = 200
@Volatile var agentRemoveStatus = 200 @Volatile var agentRemoveStatus = 200
@Volatile var skillRemoveStatus = 200
@Volatile var agentBuilderStatus = 200 @Volatile var agentBuilderStatus = 200
@Volatile var lastMcpActionPath: String? = null @Volatile var lastMcpActionPath: String? = null
@Volatile var lastAgentRemoveBody: String? = null @Volatile var lastAgentRemoveBody: String? = null
@Volatile var lastSkillRemoveBody: String? = null
@Volatile var lastAgentBuilderPath: String? = null @Volatile var lastAgentBuilderPath: String? = null
@Volatile var lastAgentBuilderBody: String? = null @Volatile var lastAgentBuilderBody: String? = null
@Volatile var lastAgentBuilderMethod: String? = null @Volatile var lastAgentBuilderMethod: String? = null
@@ -87,6 +90,12 @@ class MockCliServer : AutoCloseable {
@Volatile var commandsStatus = 200 @Volatile var commandsStatus = 200
@Volatile var skillsStatus = 200 @Volatile var skillsStatus = 200
// File search responses
@Volatile var findFiles = "[]"
@Volatile var findDirectories = "[]"
@Volatile var findFileStatus = 200
val findFilePaths = CopyOnWriteArrayList<String>()
// Session REST responses // Session REST responses
@Volatile var sessions = "[]" @Volatile var sessions = "[]"
@Volatile var recentSessions = "[]" @Volatile var recentSessions = "[]"
@@ -128,6 +137,8 @@ class MockCliServer : AutoCloseable {
@Volatile var lastSessionRenamePath: String? = null @Volatile var lastSessionRenamePath: String? = null
@Volatile var lastSessionRenameBody: String? = null @Volatile var lastSessionRenameBody: String? = null
@Volatile var lastSessionRenameMethod: String? = null @Volatile var lastSessionRenameMethod: String? = null
@Volatile var pendingPermissions = "[]"
@Volatile var pendingQuestions = "[]"
/** Configurable delay for all endpoint responses (ms). 0 = no delay. */ /** Configurable delay for all endpoint responses (ms). 0 = no delay. */
@Volatile var responseDelay: Long = 0 @Volatile var responseDelay: Long = 0
@@ -368,8 +379,18 @@ class MockCliServer : AutoCloseable {
lastAgentRemoveBody = body lastAgentRemoveBody = body
respond(output, agentRemoveStatus, if (agentRemoveStatus == 200) "true" else """{"error":"Agent not found"}""") 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 == "/command" -> respond(output, commandsStatus, commands)
bare == "/skill" -> respond(output, skillsStatus, skills) bare == "/skill" -> respond(output, skillsStatus, skills)
bare == "/find/file" -> {
findFilePaths.add(path)
val body = if (path.contains("type=directory")) findDirectories else findFiles
respond(output, findFileStatus, body)
}
bare == "/mcp" -> respond(output, mcpStatus, mcp) bare == "/mcp" -> respond(output, mcpStatus, mcp)
bare.matches(Regex("/mcp/[^/]+/(connect|disconnect)")) && method == "POST" -> { bare.matches(Regex("/mcp/[^/]+/(connect|disconnect)")) && method == "POST" -> {
lastMcpActionPath = path lastMcpActionPath = path
@@ -393,6 +414,8 @@ class MockCliServer : AutoCloseable {
respond(output, cloudSessionImportStatus, cloudSessionImport) respond(output, cloudSessionImportStatus, cloudSessionImport)
} }
bare == "/session/status" -> respond(output, sessionStatusesStatus, sessionStatuses) 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 == "GET" -> respond(output, sessionsStatus, sessions)
bare == "/session" && method == "POST" -> respond(output, sessionCreateStatus, sessionCreate) bare == "/session" && method == "POST" -> respond(output, sessionCreateStatus, sessionCreate)
bare.matches(Regex("/session/ses_[^/]+")) && method == "GET" -> bare.matches(Regex("/session/ses_[^/]+")) && method == "GET" ->
@@ -21,4 +21,13 @@ object KiloNotifications {
?: Notification(GROUP, title, content ?: "", NotificationType.ERROR) ?: Notification(GROUP, title, content ?: "", NotificationType.ERROR)
notification.notify(project) 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)
}
} }
@@ -33,7 +33,9 @@ class KiloAgentBehaviorService internal constructor(
suspend fun agents(directory: String): List<AgentDetailDto> = safe(emptyList()) { call { agents(directory) } } 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) } } 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 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 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) } } suspend fun createAgent(directory: String, input: AgentCreateDto): Boolean = safe(false) { call { createAgent(directory, input) } }
@@ -139,6 +139,7 @@ class KiloWorkspaceService internal constructor(
} }
suspend fun searchFiles(directory: String, query: String, limit: Int = 50): FileSearchResultDto { suspend fun searchFiles(directory: String, query: String, limit: Int = 50): FileSearchResultDto {
LOG.debug { "workspace file search directory=$directory query=$query limit=$limit" }
return try { return try {
call { searchFiles(directory, query, limit) } call { searchFiles(directory, query, limit) }
} catch (e: CancellationException) { } catch (e: CancellationException) {
@@ -168,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? { suspend fun localConfigTarget(directory: String): ConfigTargetDto? {
return try { return try {
val target = call { this.localConfigTarget(directory) } val target = call { this.localConfigTarget(directory) }
@@ -10,7 +10,6 @@ import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DataSink import com.intellij.openapi.actionSystem.DataSink
import com.intellij.openapi.actionSystem.IdeActions import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.actionSystem.PlatformCoreDataKeys
import com.intellij.openapi.command.undo.UndoManager import com.intellij.openapi.command.undo.UndoManager
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.editor.ex.EditorEx
@@ -74,11 +73,11 @@ internal open class SessionEditorTextField(
super.uiDataSnapshot(sink) super.uiDataSnapshot(sink)
selection?.provideCopy(sink) { text } selection?.provideCopy(sink) { text }
ctx?.let { sink.set(PromptDataKeys.SEND, it) } ctx?.let { sink.set(PromptDataKeys.SEND, it) }
file()?.let { sink.set(PlatformCoreDataKeys.FILE_EDITOR, it) }
} }
private fun install(editor: Editor) { private fun install(editor: Editor) {
(editor as? EditorEx)?.setEmbeddedIntoDialogWrapper(true) (editor as? EditorEx)?.setEmbeddedIntoDialogWrapper(true)
editor.putUserData(EditorTextField.SUPPLEMENTARY_KEY, true)
// EditorImpl lazily creates EditorFloatingToolbar with the same first-show hook. // EditorImpl lazily creates EditorFloatingToolbar with the same first-show hook.
// Settings providers run later, so this callback runs immediately after toolbar creation. // Settings providers run later, so this callback runs immediately after toolbar creation.
UiNotifyConnector.doWhenFirstShown(editor.component) { hide(editor.component) } UiNotifyConnector.doWhenFirstShown(editor.component) { hide(editor.component) }
@@ -73,8 +73,8 @@ class KiloPromptCompletionProvider(
} }
fun prewarm() { fun prewarm() {
if (cache.containsKey("")) return
scope.launch { scope.launch {
if (cache.containsKey("")) return@launch
val result = service.searchFiles(workspace.directory, "", 50) val result = service.searchFiles(workspace.directory, "", 50)
if (result.files.isNotEmpty() || result.git) cache.putIfAbsent("", result) if (result.files.isNotEmpty() || result.git) cache.putIfAbsent("", result)
} }
@@ -2,6 +2,7 @@ package ai.kilocode.client.settings
import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.settings.agents.AgentBehaviorConfigurable import ai.kilocode.client.settings.agents.AgentBehaviorConfigurable
import ai.kilocode.client.settings.context.ContextConfigurable
import ai.kilocode.client.settings.models.ModelsConfigurable import ai.kilocode.client.settings.models.ModelsConfigurable
import ai.kilocode.client.settings.providers.ProvidersConfigurable import ai.kilocode.client.settings.providers.ProvidersConfigurable
import ai.kilocode.client.settings.profile.UserProfileConfigurable import ai.kilocode.client.settings.profile.UserProfileConfigurable
@@ -73,6 +74,14 @@ class KiloSettingsConfigurable : SearchableConfigurable {
behavior.border = JBUI.Borders.emptyBottom(UiStyle.Gap.sm()) behavior.border = JBUI.Borders.emptyBottom(UiStyle.Gap.sm())
panel.next(behavior) panel.next(behavior)
val context = ActionLink(KiloBundle.message("settings.context.displayName")) { e ->
val src = e.source as? JComponent ?: return@ActionLink
val settings = Settings.KEY.getData(DataManager.getInstance().getDataContext(src)) ?: return@ActionLink
open(settings, ContextConfigurable.ID)
}
context.border = JBUI.Borders.emptyBottom(UiStyle.Gap.sm())
panel.next(context)
return panel return panel
} }
@@ -25,6 +25,7 @@ class AgentBehaviorConfigurable : SearchableConfigurable {
listOf( listOf(
KiloBundle.message("settings.agentBehavior.agents.displayName") to AgentsConfigurable.ID, KiloBundle.message("settings.agentBehavior.agents.displayName") to AgentsConfigurable.ID,
KiloBundle.message("settings.agentBehavior.mcp.displayName") to McpConfigurable.ID, KiloBundle.message("settings.agentBehavior.mcp.displayName") to McpConfigurable.ID,
KiloBundle.message("settings.agentBehavior.skills.displayName") to SkillsConfigurable.ID,
).forEach { (label, id) -> ).forEach { (label, id) ->
panel.next(ActionLink(label) { e -> panel.next(ActionLink(label) { e ->
val src = e.source as? JComponent ?: return@ActionLink val src = e.source as? JComponent ?: return@ActionLink
@@ -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
@@ -11,6 +11,8 @@ internal class SettingsDraftState<D>(
private var base = initial private var base = initial
private var pending: D? = null private var pending: D? = null
private var stale: List<D> = emptyList()
private var applied: D? = null
private var save = false private var save = false
private var err: String? = null private var err: String? = null
@@ -29,6 +31,10 @@ internal class SettingsDraftState<D>(
fun accept(next: D) { fun accept(next: D) {
val target = pending val target = pending
if (target == null) { if (target == null) {
val done = applied
if (done != null && saved(base, done) && stale.any { saved(next, it) }) return
stale = emptyList()
applied = null
val prev = base val prev = base
val edit = draft val edit = draft
base = next base = next
@@ -51,12 +57,15 @@ internal class SettingsDraftState<D>(
fun complete(token: SettingsDraftSave<D>, returned: D) { fun complete(token: SettingsDraftSave<D>, returned: D) {
val edit = draft val edit = draft
val next = if (saved(returned, token.target)) returned else token.target val fresh = saved(returned, token.target)
val next = if (fresh) returned else token.target
base = next base = next
draft = if (saved(edit, token.target)) next else edit draft = if (saved(edit, token.target)) next else edit
pending = null pending = null
save = false save = false
err = null err = null
stale += token.previous
applied = token.target
} }
fun fail(token: SettingsDraftSave<D>, message: String) { fun fail(token: SettingsDraftSave<D>, message: String) {
@@ -66,6 +75,8 @@ internal class SettingsDraftState<D>(
pending = null pending = null
save = false save = false
err = message err = message
stale = emptyList()
applied = null
} }
} }
@@ -8,6 +8,7 @@ import java.awt.Point
import java.awt.Rectangle import java.awt.Rectangle
import javax.swing.Icon import javax.swing.Icon
import javax.swing.JList import javax.swing.JList
import javax.swing.ListSelectionModel
import javax.swing.ListCellRenderer import javax.swing.ListCellRenderer
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
@@ -21,6 +22,8 @@ internal data class SettingsListConfig(
val height: SettingsListRowHeight, val height: SettingsListRowHeight,
val description: Boolean = true, val description: Boolean = true,
val descriptionIndent: Boolean = true, val descriptionIndent: Boolean = true,
val tooltip: Boolean = true,
val selection: Int = ListSelectionModel.SINGLE_SELECTION,
) { ) {
companion object { companion object {
val Equal = SettingsListConfig(SettingsListRowHeight.EQUAL) val Equal = SettingsListConfig(SettingsListRowHeight.EQUAL)
@@ -41,7 +44,9 @@ internal data class SettingsListCell(
internal interface SettingsListItem { internal interface SettingsListItem {
val key: String val key: String
val title: String val title: String
val note: String? get() = null
val description: String? get() = null val description: String? get() = null
val doubleClick: String? get() = null
val icon: Icon? get() = null val icon: Icon? get() = null
val section: String? get() = null val section: String? get() = null
val badges: List<SettingsBadge> get() = emptyList() val badges: List<SettingsBadge> get() = emptyList()
@@ -83,6 +83,9 @@ internal class SettingsListRenderer(
title.clear() title.clear()
title.append(value.title, SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, fg)) title.append(value.title, SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, fg))
value.note?.takeIf { it.isNotBlank() }?.let {
title.append(" $it", SimpleTextAttributes.GRAYED_ATTRIBUTES)
}
syncBadges(value) syncBadges(value)
icon.icon = value.icon icon.icon = value.icon
mark.isVisible = value.icon != null mark.isVisible = value.icon != null
@@ -1,6 +1,7 @@
package ai.kilocode.client.settings.base package ai.kilocode.client.settings.base
import ai.kilocode.client.session.ui.model.ModelSearch import ai.kilocode.client.session.ui.model.ModelSearch
import ai.kilocode.client.ui.UiStyle
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.ui.CollectionListModel import com.intellij.ui.CollectionListModel
import com.intellij.ui.ScrollingUtil 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.util.concurrency.annotations.RequiresEdt
import com.intellij.xml.util.XmlStringUtil import com.intellij.xml.util.XmlStringUtil
import com.intellij.util.ui.UIUtil import com.intellij.util.ui.UIUtil
import java.awt.Dimension
import java.awt.Rectangle
import java.awt.event.KeyEvent import java.awt.event.KeyEvent
import java.awt.event.MouseAdapter import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
import javax.swing.JComponent import javax.swing.JComponent
import javax.swing.KeyStroke import javax.swing.KeyStroke
import javax.swing.ListSelectionModel import javax.swing.ListSelectionModel
import javax.swing.Scrollable
import javax.swing.SwingConstants
import javax.swing.event.ListSelectionEvent import javax.swing.event.ListSelectionEvent
internal class SettingsListView( internal class SettingsListView(
empty: String, empty: String,
private val cfg: SettingsListConfig = SettingsListConfig.Equal, private val cfg: SettingsListConfig = SettingsListConfig.Equal,
private val onCell: (String, String) -> Unit, private val onCell: (String, String) -> Unit,
) : BaseContentPanel() { ) : BaseContentPanel(), Scrollable {
private val model = CollectionListModel<SettingsListItem>() private val model = CollectionListModel<SettingsListItem>()
internal val list = object : JBList<SettingsListItem>(model) { internal val list = object : JBList<SettingsListItem>(model) {
override fun getToolTipText(event: MouseEvent): String? { override fun getToolTipText(event: MouseEvent): String? {
if (!cfg.description) return null if (!cfg.description || !cfg.tooltip) return null
val idx = locationToIndex(event.point) val idx = locationToIndex(event.point)
if (idx < 0) return null if (idx < 0) return null
val bounds = getCellBounds(idx, idx) ?: return null val bounds = getCellBounds(idx, idx) ?: return null
@@ -34,7 +39,7 @@ internal class SettingsListView(
return XmlStringUtil.wrapInHtml(text) return XmlStringUtil.wrapInHtml(text)
} }
}.apply { }.apply {
selectionMode = ListSelectionModel.SINGLE_SELECTION selectionMode = cfg.selection
setExpandableItemsEnabled(false) setExpandableItemsEnabled(false)
emptyText.text = empty emptyText.text = empty
} }
@@ -67,6 +72,11 @@ internal class SettingsListView(
val hit = hit(e, enabled = false) ?: return val hit = hit(e, enabled = false) ?: return
if (hit.id != null) return if (hit.id != null) return
val item = hit.item val item = hit.item
item.doubleClick?.let { id ->
onCell(item.key, id)
e.consume()
return
}
primary(item) primary(item)
e.consume() e.consume()
} }
@@ -94,6 +104,12 @@ internal class SettingsListView(
return list.selectedValue return list.selectedValue
} }
@RequiresEdt
fun selectedItems(): List<SettingsListItem> {
checkEdt()
return list.selectedValuesList
}
@RequiresEdt @RequiresEdt
fun selectedIndex(): Int { fun selectedIndex(): Int {
checkEdt() checkEdt()
@@ -121,6 +137,7 @@ internal class SettingsListView(
@RequiresEdt @RequiresEdt
fun setBusy(value: Boolean) { fun setBusy(value: Boolean) {
checkEdt() checkEdt()
list.setPaintBusy(value)
if (list.isEnabled == !value) return if (list.isEnabled == !value) return
list.isEnabled = !value list.isEnabled = !value
list.repaint() list.repaint()
@@ -191,9 +208,15 @@ internal class SettingsListView(
private fun primary(item: SettingsListItem) { private fun primary(item: SettingsListItem) {
val cells = settingsListVisibleCells(item, true) val cells = settingsListVisibleCells(item, true)
val cell = cells.firstOrNull { it.enabled && it.primary } val cell = cells.firstOrNull { it.enabled && it.primary }
?: cells.firstOrNull { it.enabled } if (cell != null) {
?: return onCell(item.key, cell.id)
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? { 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 val bounds = idx.takeIf { it >= 0 }?.let { list.getCellBounds(it, it) } ?: return null
if (!bounds.contains(e.point)) return null if (!bounds.contains(e.point)) return null
val item = model.getElementAt(idx) val item = model.getElementAt(idx)
val selected = idx == list.selectedIndex val selected = list.isSelectedIndex(idx)
val id = if (enabled) { val id = if (enabled) {
settingsListCellAt(list, idx, e.point, selected) settingsListCellAt(list, idx, e.point, selected)
} else { } else {
@@ -217,6 +240,27 @@ internal class SettingsListView(
check(ApplicationManager.getApplication().isDispatchThread) { "Settings list updates must run on EDT" } 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 Hit(val item: SettingsListItem, val id: String?)
private data class Press(val key: String, val id: String) private data class Press(val key: String, val id: String)
@@ -33,6 +33,14 @@ internal open class SettingsPanel : SettingsOverlayPanel() {
repaint() 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 { private class SettingsBody : Stack(StackAxis.VERTICAL), Scrollable {
@@ -0,0 +1,18 @@
package ai.kilocode.client.settings.context
import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.settings.base.DraftReadyConfigurable
import kotlinx.coroutines.CoroutineScope
import javax.swing.JComponent
class ContextConfigurable : DraftReadyConfigurable<JComponent>() {
override fun getId(): String = ID
override fun getDisplayName(): String = KiloBundle.message("settings.context.displayName")
override fun create(cs: CoroutineScope): JComponent = ContextSettingsUi(cs)
companion object {
const val ID = "ai.kilocode.jetbrains.settings.context"
}
}
@@ -0,0 +1,77 @@
package ai.kilocode.client.settings.context
import ai.kilocode.rpc.dto.CompactionPatchDto
import ai.kilocode.rpc.dto.ConfigDto
import ai.kilocode.rpc.dto.ConfigPatchDto
import ai.kilocode.rpc.dto.WatcherPatchDto
internal data class ContextDraft(
val auto: Boolean = false,
val threshold: String = "",
val prune: Boolean = false,
val ignore: List<String> = emptyList(),
)
internal enum class ThresholdStatus {
VALID,
INVALID,
}
internal fun contextDraft(config: ConfigDto?): ContextDraft = ContextDraft(
auto = config?.compaction?.auto ?: false,
threshold = config?.compaction?.threshold_percent?.let(::formatThreshold).orEmpty(),
prune = config?.compaction?.prune ?: false,
ignore = config?.watcher?.ignore ?: emptyList(),
)
internal fun patch(from: ContextDraft, to: ContextDraft): ConfigPatchDto? {
if (thresholdStatus(to.threshold) == ThresholdStatus.INVALID) return null
val compaction = compactionPatch(from, to)
val watcher = if (from.ignore != to.ignore) WatcherPatchDto(ignore = to.ignore) else null
return ConfigPatchDto(watcher = watcher, compaction = compaction)
}
internal fun changed(patch: ConfigPatchDto): Boolean = patch.watcher != null || patch.compaction != null
internal fun savedMatches(base: ContextDraft, draft: ContextDraft): Boolean =
base.auto == draft.auto &&
normalizeThreshold(base.threshold) == normalizeThreshold(draft.threshold) &&
base.prune == draft.prune &&
base.ignore == draft.ignore
internal fun thresholdStatus(value: String): ThresholdStatus {
val text = value.trim()
if (text.isBlank()) return ThresholdStatus.VALID
val num = text.toDoubleOrNull()
if (num == null || !num.isFinite() || num < 0.0 || num > 100.0) return ThresholdStatus.INVALID
return ThresholdStatus.VALID
}
private fun compactionPatch(from: ContextDraft, to: ContextDraft): CompactionPatchDto? {
val threshold = parseThreshold(to.threshold)
val fromThreshold = parseThreshold(from.threshold)
val clear = if (fromThreshold != threshold && threshold == null) listOf("threshold_percent") else emptyList()
val patch = CompactionPatchDto(
clear = clear,
auto = to.auto.takeIf { from.auto != to.auto },
threshold_percent = threshold.takeIf { fromThreshold != threshold && threshold != null },
prune = to.prune.takeIf { from.prune != to.prune },
)
if (patch.clear.isEmpty() && patch.auto == null && patch.threshold_percent == null && patch.prune == null) return null
return patch
}
private fun parseThreshold(value: String): Double? {
val text = value.trim()
if (text.isBlank()) return null
return text.toDoubleOrNull()?.takeIf { it.isFinite() && it >= 0.0 && it <= 100.0 }
}
private fun normalizeThreshold(value: String): String = parseThreshold(value)?.let(::formatThreshold).orEmpty()
private fun formatThreshold(value: Double): String {
val whole = value.toLong()
if (value == whole.toDouble()) return whole.toString()
return value.toString()
}
@@ -0,0 +1,388 @@
package ai.kilocode.client.settings.context
import ai.kilocode.client.app.KiloAppService
import ai.kilocode.client.app.KiloWorkspaceService
import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.settings.base.BaseContentPanel
import ai.kilocode.client.settings.base.BaseSettingsUi
import ai.kilocode.client.settings.base.SettingsBannerKind
import ai.kilocode.client.settings.base.SettingsRow
import ai.kilocode.client.settings.base.SettingsToggle
import ai.kilocode.client.ui.HoverIcon
import ai.kilocode.client.ui.UiStyle
import ai.kilocode.client.ui.layout.HAlign
import ai.kilocode.client.ui.layout.Stack
import ai.kilocode.client.ui.layout.StackAxis
import ai.kilocode.client.ui.layout.VAlign
import ai.kilocode.client.ui.layout.align
import ai.kilocode.log.KiloLog
import ai.kilocode.rpc.dto.ConfigPatchDto
import ai.kilocode.rpc.dto.KiloAppStateDto
import ai.kilocode.rpc.dto.KiloAppStatusDto
import ai.kilocode.rpc.dto.ModelStateDto
import com.intellij.icons.AllIcons
import com.intellij.openapi.components.service
import com.intellij.openapi.ui.Messages
import com.intellij.ui.CollectionListModel
import com.intellij.ui.DocumentAdapter
import com.intellij.ui.ScrollingUtil
import com.intellij.ui.components.JBList
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBScrollPane
import com.intellij.ui.components.JBTextField
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import kotlinx.coroutines.CoroutineScope
import java.awt.event.KeyEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.JComponent
import javax.swing.DefaultListCellRenderer
import javax.swing.JList
import javax.swing.ListSelectionModel
import javax.swing.ScrollPaneConstants
import javax.swing.event.DocumentEvent
import javax.swing.text.AbstractDocument
import javax.swing.text.AttributeSet
import javax.swing.text.DocumentFilter
internal class ContextSettingsUi(
cs: CoroutineScope,
private val app: KiloAppService = service(),
workspaces: KiloWorkspaceService = service(),
) : BaseSettingsUi<ContextSettingsContent, ContextDraft, ConfigPatchDto, KiloAppStateDto, Unit>(
cs,
ContextDraft(),
app,
workspaces,
loginBanner = false,
) {
init {
startSettings(ContextSettingsContent { updateDraft(it) })
}
override fun change(from: ContextDraft, to: ContextDraft): ConfigPatchDto? = patch(from, to)?.takeIf(::changed)
override fun save(change: ConfigPatchDto, done: (KiloAppStateDto?) -> Unit) {
app.updateConfigAsync(change, done)
}
override fun base(result: KiloAppStateDto): ContextDraft = contextDraft(result.config)
override fun draft(state: KiloAppStateDto): ContextDraft = contextDraft(state.config)
override fun saved(base: ContextDraft, draft: ContextDraft): Boolean = savedMatches(base, draft)
override fun pendingText(): String = KiloBundle.message("settings.context.save.pending")
override fun failedText(): String = KiloBundle.message("settings.context.save.failed")
override suspend fun loadWorkspace(root: String) = Unit
override fun applyWorkspace(result: Unit) = Unit
override fun models(state: ModelStateDto) = Unit
override fun logSaveStarted(change: ConfigPatchDto) = LOG.info("context settings save: started ${summary(change)}")
override fun logSaveCompleted(change: ConfigPatchDto) = LOG.info("context settings save: completed ${summary(change)}")
override fun logSaveFailed(change: ConfigPatchDto) = LOG.warn("context settings save: failed ${summary(change)}")
override fun logSaveFailedAfterDispose(change: ConfigPatchDto) = LOG.warn("context settings save: failed after dispose ${summary(change)}")
override fun logSaveCompletedAfterDispose(change: ConfigPatchDto) = LOG.info("context settings save: completed after dispose ${summary(change)}")
@RequiresEdt
override fun syncContent() {
val ready = appState.status == KiloAppStatusDto.READY
val editable = ready && !saving
form.sync(draft, editable)
top.hideBanner()
val err = saveError
if (saving) {
showProgress(KiloBundle.message("settings.context.save.pending"))
return
}
if (err != null) {
showError(err)
return
}
if (!ready) {
showProgress(KiloBundle.message("settings.cli.unavailable.message"))
return
}
if (thresholdStatus(draft.threshold) == ThresholdStatus.INVALID) {
top.showBanner(
KiloBundle.message("settings.context.compaction.threshold.invalid"),
emptyList(),
SettingsBannerKind.ERROR,
)
clearProgress()
return
}
clearProgress()
}
private companion object {
val LOG = KiloLog.create(ContextSettingsUi::class.java)
}
}
internal class ContextSettingsContent(
private val update: (ContextDraft.() -> ContextDraft) -> Unit,
) : BaseContentPanel() {
private val auto = SettingsToggle { value -> update { copy(auto = value) } }
private val prune = SettingsToggle { value -> update { copy(prune = value) } }
private val threshold = ThresholdField(
KiloBundle.message("settings.context.compaction.threshold.placeholder"),
) { value -> update { copy(threshold = value) } }
private val patterns = PatternList { value -> update { copy(ignore = value) } }
init {
section(
KiloBundle.message("settings.context.compaction.title"),
).apply {
row(SettingsRow(
KiloBundle.message("settings.context.compaction.auto.title"),
KiloBundle.message("settings.context.compaction.auto.description"),
auto,
))
row(SettingsRow(
KiloBundle.message("settings.context.compaction.threshold.title"),
KiloBundle.message("settings.context.compaction.threshold.description"),
Stack.horizontal(UiStyle.Gap.xs())
.next(threshold)
.next(JBLabel(KiloBundle.message("settings.context.compaction.threshold.suffix")))
.align(HAlign.RIGHT, VAlign.CENTER),
))
row(SettingsRow(
KiloBundle.message("settings.context.compaction.prune.title"),
KiloBundle.message("settings.context.compaction.prune.description"),
prune,
))
}
section(
KiloBundle.message("settings.context.watcher.title"),
KiloBundle.message("settings.context.watcher.description"),
).row(patterns)
}
@RequiresEdt
fun sync(draft: ContextDraft, enabled: Boolean) {
auto.isSelected = draft.auto
prune.isSelected = draft.prune
threshold.sync(draft.threshold)
patterns.sync(draft.ignore)
listOf(auto, prune, threshold, patterns).forEach { it.isEnabled = enabled }
}
}
private class ThresholdField(
placeholder: String,
private val change: (String) -> Unit,
) : JBTextField() {
private var syncing = false
init {
columns = THRESHOLD_COLUMNS
emptyText.text = placeholder
(document as AbstractDocument).documentFilter = NumberFilter()
document.addDocumentListener(object : DocumentAdapter() {
override fun textChanged(e: DocumentEvent) {
if (!syncing) change(text)
}
})
}
fun sync(value: String) {
if (text == value) return
syncing = true
text = value
syncing = false
}
}
private class NumberFilter : DocumentFilter() {
override fun insertString(fb: FilterBypass, offset: Int, string: String?, attr: AttributeSet?) {
replace(fb, offset, 0, string, attr)
}
override fun replace(fb: FilterBypass, offset: Int, length: Int, text: String?, attrs: AttributeSet?) {
val value = text ?: ""
val next = StringBuilder(fb.document.getText(0, fb.document.length))
.replace(offset, offset + length, value)
.toString()
if (next.isEmpty() || valid(next)) super.replace(fb, offset, length, value, attrs)
}
private fun valid(value: String): Boolean {
if (value.count { it == '.' } > 1) return false
if (!value.all { it.isDigit() || it == '.' }) return false
val num = value.toDoubleOrNull() ?: return false
return num >= 0.0 && num <= 100.0
}
}
internal class PatternList(
private val change: (List<String>) -> Unit,
) : Stack(StackAxis.VERTICAL, UiStyle.Gap.sm()) {
private val model = CollectionListModel<String>()
internal var input: () -> String? = {
Messages.showInputDialog(
this,
KiloBundle.message("settings.context.watcher.input.prompt"),
KiloBundle.message("settings.context.watcher.input.title"),
null,
)
}
internal var editor: (String) -> String? = { value ->
Messages.showInputDialog(
this,
KiloBundle.message("settings.context.watcher.input.prompt"),
KiloBundle.message("settings.context.watcher.title"),
null,
value,
null,
)
}
private val add = HoverIcon().apply {
icon = AllIcons.General.Add
toolTipText = KiloBundle.message("settings.context.watcher.add")
addActionListener { add() }
}
private val remove = HoverIcon().apply {
icon = AllIcons.General.Remove
toolTipText = KiloBundle.message("settings.context.watcher.remove")
addActionListener { remove() }
}
private val list = JBList(model).apply {
selectionMode = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION
isFocusable = true
emptyText.text = KiloBundle.message("settings.context.watcher.empty")
cellRenderer = PatternRenderer()
}
private val toolbar = Stack.horizontal().next(add).next(remove)
private val scroll = JBScrollPane(list).apply {
border = null
viewportBorder = null
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
}
init {
border = JBUI.Borders.empty(UiStyle.Gap.pad(), 0, UiStyle.Gap.pad(), 0)
list.addListSelectionListener { if (!it.valueIsAdjusting) syncActions() }
list.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
if (e.clickCount != 2 || !UIUtil.isActionClick(e, MouseEvent.MOUSE_CLICKED, true)) return
val idx = list.locationToIndex(e.point)
if (idx < 0 || list.getCellBounds(idx, idx)?.contains(e.point) != true) return
edit(idx)
}
})
list.registerKeyboardAction(
{ remove() },
javax.swing.KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0),
JComponent.WHEN_FOCUSED,
)
ScrollingUtil.installActions(list)
next(toolbar.align(HAlign.LEFT, VAlign.CENTER))
gap(UiStyle.Gap.sm())
next(scroll)
syncActions()
}
@RequiresEdt
fun sync(values: List<String>) {
if (model.items != values) model.replaceAll(values)
syncActions()
}
override fun setEnabled(enabled: Boolean) {
super.setEnabled(enabled)
add.isEnabled = enabled
remove.isEnabled = enabled && list.selectedIndices.isNotEmpty()
list.isEnabled = enabled
scroll.isEnabled = enabled
toolbar.isEnabled = enabled
syncActions()
}
private fun add() {
if (!isEnabled) return
val value = input()?.trim().orEmpty()
if (value.isBlank()) return
val values = model.items.toMutableList()
val idx = values.indexOf(value).takeIf { it >= 0 } ?: run {
values += value
model.replaceAll(values)
change(values)
values.lastIndex
}
list.selectedIndex = idx
ScrollingUtil.ensureIndexIsVisible(list, idx, 0)
syncActions()
}
private fun edit(idx: Int) {
if (!isEnabled || idx < 0 || idx >= model.size) return
val value = editor(model.getElementAt(idx))?.trim().orEmpty()
if (value.isBlank()) return
val values = model.items.toMutableList()
val found = values.indexOf(value)
val next = if (found >= 0 && found != idx) {
values.removeAt(idx)
if (found > idx) found - 1 else found
} else {
values[idx] = value
idx
}
model.replaceAll(values)
change(values)
list.selectedIndex = next
ScrollingUtil.ensureIndexIsVisible(list, next, 0)
syncActions()
}
private fun remove() {
val indices = list.selectedIndices.filter { it >= 0 && it < model.size }
if (!isEnabled || indices.isEmpty()) return
val values = model.items.toMutableList()
indices.sortedDescending().forEach(values::removeAt)
model.replaceAll(values)
val next = indices.minOrNull()?.coerceAtMost(values.lastIndex) ?: -1
if (next >= 0) list.selectedIndex = next else list.clearSelection()
change(values)
syncActions()
}
private fun syncActions() {
add.isEnabled = isEnabled
remove.isEnabled = isEnabled && list.selectedIndices.isNotEmpty()
}
private class PatternRenderer : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
selected: Boolean,
focus: Boolean,
): java.awt.Component {
val comp = super.getListCellRendererComponent(list, value, index, selected, focus) as JComponent
comp.border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap())
return comp
}
}
}
private fun summary(patch: ConfigPatchDto): String {
val parts = listOfNotNull(
"watcher".takeIf { patch.watcher != null },
"compaction".takeIf { patch.compaction != null },
)
return parts.joinToString(",").ifEmpty { "none" }
}
private const val THRESHOLD_COLUMNS = 8
@@ -33,7 +33,7 @@
<applicationConfigurable <applicationConfigurable
parentId="ai.kilocode.jetbrains.settings" parentId="ai.kilocode.jetbrains.settings"
id="ai.kilocode.jetbrains.settings.profile" id="ai.kilocode.jetbrains.settings.profile"
groupWeight="4" groupWeight="5"
instance="ai.kilocode.client.settings.profile.UserProfileConfigurable" instance="ai.kilocode.client.settings.profile.UserProfileConfigurable"
bundle="messages.KiloBundle" bundle="messages.KiloBundle"
key="settings.profile.displayName"/> key="settings.profile.displayName"/>
@@ -41,7 +41,7 @@
<applicationConfigurable <applicationConfigurable
parentId="ai.kilocode.jetbrains.settings" parentId="ai.kilocode.jetbrains.settings"
id="ai.kilocode.jetbrains.settings.models" id="ai.kilocode.jetbrains.settings.models"
groupWeight="3" groupWeight="4"
instance="ai.kilocode.client.settings.models.ModelsConfigurable" instance="ai.kilocode.client.settings.models.ModelsConfigurable"
bundle="messages.KiloBundle" bundle="messages.KiloBundle"
key="settings.models.displayName"/> key="settings.models.displayName"/>
@@ -62,6 +62,14 @@
bundle="messages.KiloBundle" bundle="messages.KiloBundle"
key="settings.agentBehavior.displayName"/> key="settings.agentBehavior.displayName"/>
<applicationConfigurable
parentId="ai.kilocode.jetbrains.settings"
id="ai.kilocode.jetbrains.settings.context"
groupWeight="0"
instance="ai.kilocode.client.settings.context.ContextConfigurable"
bundle="messages.KiloBundle"
key="settings.context.displayName"/>
<applicationConfigurable <applicationConfigurable
parentId="ai.kilocode.jetbrains.settings.agentBehavior" parentId="ai.kilocode.jetbrains.settings.agentBehavior"
id="ai.kilocode.jetbrains.settings.agentBehavior.agents" id="ai.kilocode.jetbrains.settings.agentBehavior.agents"
@@ -76,6 +84,13 @@
bundle="messages.KiloBundle" bundle="messages.KiloBundle"
key="settings.agentBehavior.mcp.displayName"/> 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" <registryKey key="kilo.session.condense"
description="Enable event condensing in the session update queue (merges redundant snapshots before model delivery)." description="Enable event condensing in the session update queue (merges redundant snapshots before model delivery)."
defaultValue="true" defaultValue="true"
@@ -1,4 +1,6 @@
common.delete=Delete common.delete=Delete
common.open=Open
common.save=Save
session.action.cancel=Cancel session.action.cancel=Cancel
session.connection.connecting=Loading... session.connection.connecting=Loading...
@@ -303,6 +305,27 @@ settings.kilo.description=Configure Kilo Code AI coding assistant features and a
settings.cli.unavailable.title=Kilo Code is not connected settings.cli.unavailable.title=Kilo Code is not connected
settings.cli.unavailable.message=Settings are available after Kilo Code connects to Core. If this does not resolve, restart the IDE or Kilo Core and try again. settings.cli.unavailable.message=Settings are available after Kilo Code connects to Core. If this does not resolve, restart the IDE or Kilo Core and try again.
settings.models.displayName=Models settings.models.displayName=Models
settings.context.displayName=Context
settings.context.description=Configure context behavior.
settings.context.save.pending=Saving context settings...
settings.context.save.failed=Failed to save context settings
settings.context.compaction.title=Compaction
settings.context.compaction.auto.title=Auto Compaction
settings.context.compaction.auto.description=Automatically compact context before it reaches the limit
settings.context.compaction.threshold.title=Auto Compaction Limit
settings.context.compaction.threshold.description=Compact when context reaches this percentage of the model window. Leave blank to use the safety buffer only.
settings.context.compaction.threshold.placeholder=Default
settings.context.compaction.threshold.suffix=%
settings.context.compaction.threshold.invalid=Enter a number from 0 to 100, or leave the field blank.
settings.context.compaction.prune.title=Prune Old Outputs
settings.context.compaction.prune.description=Remove old tool outputs during compaction
settings.context.watcher.title=File Watcher Ignore Patterns
settings.context.watcher.description=Glob patterns for files the watcher should ignore
settings.context.watcher.add=Add pattern
settings.context.watcher.remove=Remove selected patterns
settings.context.watcher.empty=No ignore patterns configured.
settings.context.watcher.input.title=Add ignore pattern
settings.context.watcher.input.prompt=Enter a glob pattern to ignore:
settings.providers.displayName=Providers settings.providers.displayName=Providers
settings.agentBehavior.displayName=Agent Behavior settings.agentBehavior.displayName=Agent Behavior
settings.agentBehavior.description=Configure agents, MCP servers, rules, workflows, and skills. settings.agentBehavior.description=Configure agents, MCP servers, rules, workflows, and skills.
@@ -436,6 +459,31 @@ settings.agentBehavior.mcp.status.failed=failed
settings.agentBehavior.mcp.status.needsAuth=needs auth settings.agentBehavior.mcp.status.needsAuth=needs auth
settings.agentBehavior.mcp.status.needsRegistration=needs registration settings.agentBehavior.mcp.status.needsRegistration=needs registration
settings.agentBehavior.mcp.status.disabled=disabled 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.loading=Loading providers...
settings.providers.connected=Connected providers settings.providers.connected=Connected providers
settings.providers.available=Available providers settings.providers.available=Available providers
@@ -1,4 +1,6 @@
common.delete=حذف common.delete=حذف
common.open=فتح
common.save=حفظ
session.action.cancel=إلغاء session.action.cancel=إلغاء
session.connection.connecting=جاري التحميل… session.connection.connecting=جاري التحميل…
session.connection.error.app=فشل الاتصال session.connection.error.app=فشل الاتصال
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.agents.create.failed=Could not create the agent.
settings.agentBehavior.mcp.addHint=لإضافة خادم MCP، اطلب من الوكيل إضافته. 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. 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.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
revert.banner.count.one=تم التراجع عن رسالة واحدة revert.banner.count.one=تم التراجع عن رسالة واحدة
@@ -376,3 +403,26 @@ revert.banner.redo=إعادة
revert.banner.redo.all=إعادة الكل revert.banner.redo.all=إعادة الكل
revert.banner.hint=يمكنك إعادة هذه التغييرات حتى ترسل رسالة جديدة revert.banner.hint=يمكنك إعادة هذه التغييرات حتى ترسل رسالة جديدة
revert.message.rollback=العودة إلى هذه الرسالة revert.message.rollback=العودة إلى هذه الرسالة
# Context settings
settings.context.displayName=السياق
settings.context.description=تكوين سلوك السياق.
settings.context.save.pending=جارٍ حفظ إعدادات السياق...
settings.context.save.failed=فشل حفظ إعدادات السياق
settings.context.compaction.title=الضغط
settings.context.compaction.auto.title=ضغط تلقائي
settings.context.compaction.auto.description=ضغط السياق تلقائياً قبل أن يصل إلى الحد
settings.context.compaction.threshold.title=حد الضغط التلقائي
settings.context.compaction.threshold.description=اضغط عندما يصل السياق إلى هذه النسبة المئوية من نافذة النموذج. اتركه فارغاً لاستخدام هامش الأمان فقط.
settings.context.compaction.threshold.placeholder=افتراضي
settings.context.compaction.threshold.suffix=%
settings.context.compaction.threshold.invalid=أدخل رقماً من 0 إلى 100، أو اترك الحقل فارغاً.
settings.context.compaction.prune.title=تقليم المخرجات القديمة
settings.context.compaction.prune.description=إزالة مخرجات الأدوات القديمة أثناء الضغط
settings.context.watcher.title=أنماط تجاهل مراقب الملفات
settings.context.watcher.description=أنماط glob للملفات التي يجب على المراقب تجاهلها
settings.context.watcher.add=إضافة نمط
settings.context.watcher.remove=إزالة الأنماط المحددة
settings.context.watcher.empty=لم يتم تكوين أنماط تجاهل.
settings.context.watcher.input.title=إضافة نمط تجاهل
settings.context.watcher.input.prompt=أدخل نمط glob لتجاهله:
@@ -1,4 +1,6 @@
common.delete=Obriši common.delete=Obriši
common.open=Otvori
common.save=Sačuvaj
session.action.cancel=Otkaži session.action.cancel=Otkaži
session.connection.connecting=Učitavanje… session.connection.connecting=Učitavanje…
session.connection.error.app=Greška pri spajanju 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.agents.create.failed=Could not create the agent.
settings.agentBehavior.mcp.addHint=Da dodate MCP server, zamolite agenta da ga doda. 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. 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.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
revert.banner.count.one={0} poruka vraćena revert.banner.count.one={0} poruka vraćena
@@ -376,3 +403,26 @@ revert.banner.redo=Ponovi
revert.banner.redo.all=Ponovi sve revert.banner.redo.all=Ponovi sve
revert.banner.hint=Možete ponoviti ove promjene dok ne pošaljete novu poruku revert.banner.hint=Možete ponoviti ove promjene dok ne pošaljete novu poruku
revert.message.rollback=Vrati na ovu poruku revert.message.rollback=Vrati na ovu poruku
# Context settings
settings.context.displayName=Kontekst
settings.context.description=Konfigurišite ponašanje konteksta.
settings.context.save.pending=Spremanje postavki konteksta...
settings.context.save.failed=Spremanje postavki konteksta nije uspjelo
settings.context.compaction.title=Kompresija
settings.context.compaction.auto.title=Automatska kompresija
settings.context.compaction.auto.description=Automatski komprimiraj kontekst prije nego dostigne limit
settings.context.compaction.threshold.title=Limit automatske kompresije
settings.context.compaction.threshold.description=Komprimiraj kada kontekst dostigne ovaj procenat prozora modela. Ostavite prazno da koristite samo sigurnosnu rezervu.
settings.context.compaction.threshold.placeholder=Zadano
settings.context.compaction.threshold.suffix=%
settings.context.compaction.threshold.invalid=Unesite broj od 0 do 100 ili ostavite polje prazno.
settings.context.compaction.prune.title=Očisti stare izlaze
settings.context.compaction.prune.description=Ukloni stare izlaze alata tokom kompresije
settings.context.watcher.title=Uzorci ignoriranja za promatrač datoteka
settings.context.watcher.description=Glob uzorci za datoteke koje promatrač treba ignorirati
settings.context.watcher.add=Dodaj uzorak
settings.context.watcher.remove=Ukloni odabrane uzorke
settings.context.watcher.empty=Nema konfigurisanih uzoraka za ignoriranje.
settings.context.watcher.input.title=Dodaj uzorak za ignoriranje
settings.context.watcher.input.prompt=Unesite glob uzorak za ignoriranje:
@@ -1,4 +1,6 @@
common.delete=Slet common.delete=Slet
common.open=Åbn
common.save=Gem
session.action.cancel=Annuller session.action.cancel=Annuller
session.connection.connecting=Indlæser… session.connection.connecting=Indlæser…
session.connection.error.app=Forbindelsesfejl 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.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.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. 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.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
revert.banner.count.one={0} besked rullet tilbage revert.banner.count.one={0} besked rullet tilbage
@@ -376,3 +403,26 @@ revert.banner.redo=Gentag
revert.banner.redo.all=Gentag alle revert.banner.redo.all=Gentag alle
revert.banner.hint=Du kan gentage disse ændringer, indtil du sender en ny besked revert.banner.hint=Du kan gentage disse ændringer, indtil du sender en ny besked
revert.message.rollback=Rul tilbage til denne besked revert.message.rollback=Rul tilbage til denne besked
# Context settings
settings.context.displayName=Kontekst
settings.context.description=Konfigurer kontekstadfærd.
settings.context.save.pending=Gemmer kontekstindstillinger...
settings.context.save.failed=Kunne ikke gemme kontekstindstillinger
settings.context.compaction.title=Komprimering
settings.context.compaction.auto.title=Automatisk komprimering
settings.context.compaction.auto.description=Komprimér automatisk kontekst, før den når grænsen
settings.context.compaction.threshold.title=Grænse for automatisk komprimering
settings.context.compaction.threshold.description=Komprimér, når konteksten når denne procentdel af modelvinduet. Lad feltet være tomt for kun at bruge sikkerhedsbufferen.
settings.context.compaction.threshold.placeholder=Standard
settings.context.compaction.threshold.suffix=%
settings.context.compaction.threshold.invalid=Indtast et tal fra 0 til 100, eller lad feltet være tomt.
settings.context.compaction.prune.title=Fjern gamle output
settings.context.compaction.prune.description=Fjern gamle værktøjsoutput under komprimering
settings.context.watcher.title=Filvagt-ignormønstre
settings.context.watcher.description=Glob-mønstre for filer, som vagten skal ignorere
settings.context.watcher.add=Tilføj mønster
settings.context.watcher.remove=Fjern valgte mønstre
settings.context.watcher.empty=Ingen ignormønstre konfigureret.
settings.context.watcher.input.title=Tilføj ignormønster
settings.context.watcher.input.prompt=Indtast et glob-mønster, der skal ignoreres:
@@ -1,4 +1,6 @@
common.delete=Löschen common.delete=Löschen
common.open=Öffnen
common.save=Speichern
session.action.cancel=Abbrechen session.action.cancel=Abbrechen
session.connection.connecting=Wird geladen… session.connection.connecting=Wird geladen…
session.connection.error.app=Verbindung fehlgeschlagen 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.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.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. 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.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
revert.banner.count.one={0} Nachricht zurückgesetzt revert.banner.count.one={0} Nachricht zurückgesetzt
@@ -376,3 +403,26 @@ revert.banner.redo=Wiederholen
revert.banner.redo.all=Alle wiederholen revert.banner.redo.all=Alle wiederholen
revert.banner.hint=Du kannst diese Änderungen wiederholen, bis du eine neue Nachricht sendest revert.banner.hint=Du kannst diese Änderungen wiederholen, bis du eine neue Nachricht sendest
revert.message.rollback=Auf diese Nachricht zurücksetzen revert.message.rollback=Auf diese Nachricht zurücksetzen
# Context settings
settings.context.displayName=Kontext
settings.context.description=Kontextverhalten konfigurieren.
settings.context.save.pending=Kontexteinstellungen werden gespeichert...
settings.context.save.failed=Kontexteinstellungen konnten nicht gespeichert werden
settings.context.compaction.title=Komprimierung
settings.context.compaction.auto.title=Automatische Komprimierung
settings.context.compaction.auto.description=Kontext automatisch komprimieren, bevor er das Limit erreicht
settings.context.compaction.threshold.title=Limit für automatische Komprimierung
settings.context.compaction.threshold.description=Komprimieren, wenn der Kontext diesen Prozentsatz des Modellfensters erreicht. Leer lassen, um nur den Sicherheitspuffer zu verwenden.
settings.context.compaction.threshold.placeholder=Standard
settings.context.compaction.threshold.suffix=%
settings.context.compaction.threshold.invalid=Geben Sie eine Zahl von 0 bis 100 ein oder lassen Sie das Feld leer.
settings.context.compaction.prune.title=Alte Ausgaben bereinigen
settings.context.compaction.prune.description=Alte Werkzeugausgaben während der Komprimierung entfernen
settings.context.watcher.title=Datei-Watcher-Ignorierungsmuster
settings.context.watcher.description=Glob-Muster für Dateien, die der Watcher ignorieren soll
settings.context.watcher.add=Muster hinzufügen
settings.context.watcher.remove=Ausgewählte Muster entfernen
settings.context.watcher.empty=Keine Ignorierungsmuster konfiguriert.
settings.context.watcher.input.title=Ignorierungsmuster hinzufügen
settings.context.watcher.input.prompt=Glob-Muster zum Ignorieren eingeben:
@@ -1,4 +1,6 @@
common.delete=Eliminar common.delete=Eliminar
common.open=Abrir
common.save=Guardar
session.action.cancel=Cancelar session.action.cancel=Cancelar
session.connection.connecting=Cargando… session.connection.connecting=Cargando…
session.connection.error.app=Error de conexión 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.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.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. 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.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
revert.banner.count.one={0} mensaje revertido revert.banner.count.one={0} mensaje revertido
@@ -376,3 +403,26 @@ revert.banner.redo=Rehacer
revert.banner.redo.all=Rehacer todo revert.banner.redo.all=Rehacer todo
revert.banner.hint=Puedes rehacer estos cambios hasta que envíes un mensaje nuevo revert.banner.hint=Puedes rehacer estos cambios hasta que envíes un mensaje nuevo
revert.message.rollback=Revertir a este mensaje revert.message.rollback=Revertir a este mensaje
# Context settings
settings.context.displayName=Contexto
settings.context.description=Configura el comportamiento del contexto.
settings.context.save.pending=Guardando configuración de contexto...
settings.context.save.failed=Error al guardar la configuración de contexto
settings.context.compaction.title=Compactación
settings.context.compaction.auto.title=Compactación automática
settings.context.compaction.auto.description=Compactar automáticamente el contexto antes de que alcance el límite
settings.context.compaction.threshold.title=Límite de compactación automática
settings.context.compaction.threshold.description=Compactar cuando el contexto alcance este porcentaje de la ventana del modelo. Déjalo en blanco para usar solo el búfer de seguridad.
settings.context.compaction.threshold.placeholder=Predeterminado
settings.context.compaction.threshold.suffix=%
settings.context.compaction.threshold.invalid=Introduce un número de 0 a 100 o deja el campo en blanco.
settings.context.compaction.prune.title=Eliminar salidas antiguas
settings.context.compaction.prune.description=Eliminar salidas de herramientas antiguas durante la compactación
settings.context.watcher.title=Patrones de ignorar del observador
settings.context.watcher.description=Patrones glob para archivos que el observador debe ignorar
settings.context.watcher.add=Agregar patrón
settings.context.watcher.remove=Eliminar patrones seleccionados
settings.context.watcher.empty=No hay patrones de ignorar configurados.
settings.context.watcher.input.title=Agregar patrón de ignorar
settings.context.watcher.input.prompt=Introduce un patrón glob para ignorar:
@@ -1,4 +1,6 @@
common.delete=Supprimer common.delete=Supprimer
common.open=Ouvrir
common.save=Enregistrer
session.action.cancel=Annuler session.action.cancel=Annuler
session.connection.connecting=Chargement… session.connection.connecting=Chargement…
session.connection.error.app=Échec de la connexion 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.agents.create.failed=Could not create the agent.
settings.agentBehavior.mcp.addHint=Pour ajouter un serveur MCP, demandez à lagent de le faire. settings.agentBehavior.mcp.addHint=Pour ajouter un serveur MCP, demandez à lagent 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 souvrira après la fermeture des paramètres.
settings.agentBehavior.skills.openInEditor.failed=Impossible douvrir 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 lURL des compétences
session.file.missing=Couldn''t find ''{0}'' in this repository. 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.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
revert.banner.count.one={0} message annulé revert.banner.count.one={0} message annulé
@@ -376,3 +403,26 @@ revert.banner.redo=Rétablir
revert.banner.redo.all=Tout rétablir revert.banner.redo.all=Tout rétablir
revert.banner.hint=Vous pouvez rétablir ces modifications jusqu'à l'envoi d'un nouveau message revert.banner.hint=Vous pouvez rétablir ces modifications jusqu'à l'envoi d'un nouveau message
revert.message.rollback=Revenir à ce message revert.message.rollback=Revenir à ce message
# Context settings
settings.context.displayName=Contexte
settings.context.description=Configurer le comportement du contexte.
settings.context.save.pending=Enregistrement des paramètres de contexte...
settings.context.save.failed=Échec de lenregistrement des paramètres de contexte
settings.context.compaction.title=Compactage
settings.context.compaction.auto.title=Compaction automatique
settings.context.compaction.auto.description=Compacter automatiquement le contexte avant quil natteigne la limite
settings.context.compaction.threshold.title=Limite de compactage automatique
settings.context.compaction.threshold.description=Compacter lorsque le contexte atteint ce pourcentage de la fenêtre du modèle. Laissez vide pour utiliser uniquement la marge de sécurité.
settings.context.compaction.threshold.placeholder=Par défaut
settings.context.compaction.threshold.suffix=%
settings.context.compaction.threshold.invalid=Saisissez un nombre de 0 à 100, ou laissez le champ vide.
settings.context.compaction.prune.title=Élaguer les anciennes sorties
settings.context.compaction.prune.description=Supprimer les anciennes sorties doutils pendant la compaction
settings.context.watcher.title=Motifs dignorance de lobservateur
settings.context.watcher.description=Motifs glob pour les fichiers que lobservateur doit ignorer
settings.context.watcher.add=Ajouter un motif
settings.context.watcher.remove=Supprimer les motifs sélectionnés
settings.context.watcher.empty=Aucun motif dignorance configuré.
settings.context.watcher.input.title=Ajouter un motif dignorance
settings.context.watcher.input.prompt=Saisissez un motif glob à ignorer :
@@ -1,4 +1,6 @@
common.delete=削除 common.delete=削除
common.open=開く
common.save=保存
session.action.cancel=キャンセル session.action.cancel=キャンセル
session.connection.connecting=読み込み中… session.connection.connecting=読み込み中…
session.connection.error.app=接続に失敗しました session.connection.error.app=接続に失敗しました
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.agents.create.failed=Could not create the agent.
settings.agentBehavior.mcp.addHint=MCPサーバーを追加するには、エージェントに依頼してください。 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. 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.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
revert.banner.count.one={0} 件のメッセージをロールバックしました revert.banner.count.one={0} 件のメッセージをロールバックしました
@@ -376,3 +403,26 @@ revert.banner.redo=やり直し
revert.banner.redo.all=すべてやり直し revert.banner.redo.all=すべてやり直し
revert.banner.hint=新しいメッセージを送信するまで、これらの変更をやり直せます revert.banner.hint=新しいメッセージを送信するまで、これらの変更をやり直せます
revert.message.rollback=このメッセージまでロールバック revert.message.rollback=このメッセージまでロールバック
# Context settings
settings.context.displayName=コンテキスト
settings.context.description=コンテキストの動作を設定します。
settings.context.save.pending=コンテキスト設定を保存しています...
settings.context.save.failed=コンテキスト設定の保存に失敗しました
settings.context.compaction.title=圧縮
settings.context.compaction.auto.title=自動圧縮
settings.context.compaction.auto.description=コンテキストが上限に達する前に自動的に圧縮
settings.context.compaction.threshold.title=自動圧縮の上限
settings.context.compaction.threshold.description=コンテキストがモデルウィンドウのこの割合に達したら圧縮します。安全バッファーのみを使用するには空欄のままにしてください。
settings.context.compaction.threshold.placeholder=デフォルト
settings.context.compaction.threshold.suffix=%
settings.context.compaction.threshold.invalid=0から100までの数値を入力するか、空欄のままにしてください。
settings.context.compaction.prune.title=古い出力を削除
settings.context.compaction.prune.description=圧縮時に古いツール出力を削除
settings.context.watcher.title=ファイルウォッチャー無視パターン
settings.context.watcher.description=ウォッチャーが無視すべきファイルのglobパターン
settings.context.watcher.add=パターンを追加
settings.context.watcher.remove=選択したパターンを削除
settings.context.watcher.empty=無視パターンは設定されていません。
settings.context.watcher.input.title=無視パターンを追加
settings.context.watcher.input.prompt=無視するglobパターンを入力してください:
@@ -1,4 +1,6 @@
common.delete=삭제 common.delete=삭제
common.open=열기
common.save=저장
session.action.cancel=취소 session.action.cancel=취소
session.connection.connecting=로딩 중… session.connection.connecting=로딩 중…
session.connection.error.app=연결 실패 session.connection.error.app=연결 실패
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.agents.create.failed=Could not create the agent.
settings.agentBehavior.mcp.addHint=MCP 서버를 추가하려면 에이전트에게 요청하세요. 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. 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.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
revert.banner.count.one={0}개 메시지가 롤백됨 revert.banner.count.one={0}개 메시지가 롤백됨
@@ -376,3 +403,26 @@ revert.banner.redo=다시 실행
revert.banner.redo.all=모두 다시 실행 revert.banner.redo.all=모두 다시 실행
revert.banner.hint=새 메시지를 보내기 전까지 이 변경 사항을 다시 실행할 수 있습니다 revert.banner.hint=새 메시지를 보내기 전까지 이 변경 사항을 다시 실행할 수 있습니다
revert.message.rollback=이 메시지로 롤백 revert.message.rollback=이 메시지로 롤백
# Context settings
settings.context.displayName=컨텍스트
settings.context.description=컨텍스트 동작을 구성합니다.
settings.context.save.pending=컨텍스트 설정 저장 중...
settings.context.save.failed=컨텍스트 설정을 저장하지 못했습니다
settings.context.compaction.title=압축
settings.context.compaction.auto.title=자동 압축
settings.context.compaction.auto.description=컨텍스트가 한도에 도달하기 전에 자동으로 압축
settings.context.compaction.threshold.title=자동 압축 한도
settings.context.compaction.threshold.description=컨텍스트가 모델 창의 이 비율에 도달하면 압축합니다. 안전 버퍼만 사용하려면 비워 두세요.
settings.context.compaction.threshold.placeholder=기본값
settings.context.compaction.threshold.suffix=%
settings.context.compaction.threshold.invalid=0에서 100 사이의 숫자를 입력하거나 필드를 비워 두세요.
settings.context.compaction.prune.title=이전 출력 정리
settings.context.compaction.prune.description=압축 중 이전 도구 출력 제거
settings.context.watcher.title=파일 감시자 무시 패턴
settings.context.watcher.description=감시자가 무시해야 할 파일의 글로브 패턴
settings.context.watcher.add=패턴 추가
settings.context.watcher.remove=선택한 패턴 제거
settings.context.watcher.empty=구성된 무시 패턴이 없습니다.
settings.context.watcher.input.title=무시 패턴 추가
settings.context.watcher.input.prompt=무시할 glob 패턴을 입력하세요:
@@ -1,4 +1,6 @@
common.delete=Verwijderen common.delete=Verwijderen
common.open=Openen
common.save=Opslaan
session.action.cancel=Annuleren session.action.cancel=Annuleren
session.connection.connecting=Laden… session.connection.connecting=Laden…
session.connection.error.app=Verbinding mislukt 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.agents.create.failed=Could not create the agent.
settings.agentBehavior.mcp.addHint=Vraag de agent om een MCP-server toe te voegen. 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 URLs 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. 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.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
revert.banner.count.one={0} bericht teruggedraaid revert.banner.count.one={0} bericht teruggedraaid
@@ -376,3 +403,26 @@ revert.banner.redo=Opnieuw uitvoeren
revert.banner.redo.all=Alles opnieuw uitvoeren revert.banner.redo.all=Alles opnieuw uitvoeren
revert.banner.hint=Je kunt deze wijzigingen opnieuw uitvoeren totdat je een nieuw bericht verstuurt revert.banner.hint=Je kunt deze wijzigingen opnieuw uitvoeren totdat je een nieuw bericht verstuurt
revert.message.rollback=Terugdraaien naar dit bericht revert.message.rollback=Terugdraaien naar dit bericht
# Context settings
settings.context.displayName=Context
settings.context.description=Contextgedrag configureren.
settings.context.save.pending=Contextinstellingen opslaan...
settings.context.save.failed=Contextinstellingen opslaan mislukt
settings.context.compaction.title=Compactie
settings.context.compaction.auto.title=Automatische Compactie
settings.context.compaction.auto.description=Context automatisch compacteren voordat deze de limiet bereikt
settings.context.compaction.threshold.title=Limiet voor automatisch compacteren
settings.context.compaction.threshold.description=Compacteer wanneer de context dit percentage van het modelvenster bereikt. Laat leeg om alleen de veiligheidsbuffer te gebruiken.
settings.context.compaction.threshold.placeholder=Standaard
settings.context.compaction.threshold.suffix=%
settings.context.compaction.threshold.invalid=Voer een getal van 0 tot 100 in, of laat het veld leeg.
settings.context.compaction.prune.title=Oude Uitvoer Opschonen
settings.context.compaction.prune.description=Verwijder oude tool uitvoer tijdens compactie
settings.context.watcher.title=File Watcher Negeer Patronen
settings.context.watcher.description=Glob-patronen voor bestanden die de watcher moet negeren
settings.context.watcher.add=Patroon toevoegen
settings.context.watcher.remove=Geselecteerde patronen verwijderen
settings.context.watcher.empty=Geen negeerpatronen geconfigureerd.
settings.context.watcher.input.title=Negeerpatroon toevoegen
settings.context.watcher.input.prompt=Voer een glob-patroon in om te negeren:
@@ -1,4 +1,6 @@
common.delete=Slett common.delete=Slett
common.open=Åpne
common.save=Lagre
session.action.cancel=Avbryt session.action.cancel=Avbryt
session.connection.connecting=Laster… session.connection.connecting=Laster…
session.connection.error.app=Tilkoblingsfeil 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.agents.create.failed=Could not create the agent.
settings.agentBehavior.mcp.addHint=Be agenten om å legge til en MCP-server. 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. 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.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
revert.banner.count.one={0} melding rullet tilbake revert.banner.count.one={0} melding rullet tilbake
@@ -376,3 +403,26 @@ revert.banner.redo=Gjør om
revert.banner.redo.all=Gjør om alle revert.banner.redo.all=Gjør om alle
revert.banner.hint=Du kan gjøre om disse endringene til du sender en ny melding revert.banner.hint=Du kan gjøre om disse endringene til du sender en ny melding
revert.message.rollback=Rull tilbake til denne meldingen revert.message.rollback=Rull tilbake til denne meldingen
# Context settings
settings.context.displayName=Kontekst
settings.context.description=Konfigurer kontekstadferd.
settings.context.save.pending=Lagrer kontekstinnstillinger...
settings.context.save.failed=Kunne ikke lagre kontekstinnstillinger
settings.context.compaction.title=Komprimering
settings.context.compaction.auto.title=Automatisk komprimering
settings.context.compaction.auto.description=Komprimer automatisk kontekst før den når grensen
settings.context.compaction.threshold.title=Grense for automatisk komprimering
settings.context.compaction.threshold.description=Komprimer når konteksten når denne prosentandelen av modellvinduet. La stå tomt for å bare bruke sikkerhetsbufferen.
settings.context.compaction.threshold.placeholder=Standard
settings.context.compaction.threshold.suffix=%
settings.context.compaction.threshold.invalid=Skriv inn et tall fra 0 til 100, eller la feltet stå tomt.
settings.context.compaction.prune.title=Fjern gamle utdata
settings.context.compaction.prune.description=Fjern gamle verktøyutdata under komprimering
settings.context.watcher.title=Filvakt-ignormønstre
settings.context.watcher.description=Glob-mønstre for filer som vakten skal ignorere
settings.context.watcher.add=Legg til mønster
settings.context.watcher.remove=Fjern valgte mønstre
settings.context.watcher.empty=Ingen ignormønstre konfigurert.
settings.context.watcher.input.title=Legg til ignormønster
settings.context.watcher.input.prompt=Skriv inn et glob-mønster som skal ignoreres:
@@ -1,4 +1,6 @@
common.delete=Usuń common.delete=Usuń
common.open=Otwórz
common.save=Zapisz
session.action.cancel=Anuluj session.action.cancel=Anuluj
session.connection.connecting=Ładowanie… session.connection.connecting=Ładowanie…
session.connection.error.app=Błąd połączenia 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.agents.create.failed=Could not create the agent.
settings.agentBehavior.mcp.addHint=Aby dodać serwer MCP, poproś agenta, aby to zrobił. 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. 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.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
revert.banner.count.one=Cofnięto {0} wiadomość revert.banner.count.one=Cofnięto {0} wiadomość
@@ -376,3 +403,26 @@ revert.banner.redo=Ponów
revert.banner.redo.all=Ponów wszystko revert.banner.redo.all=Ponów wszystko
revert.banner.hint=Możesz ponowić te zmiany, dopóki nie wyślesz nowej wiadomości revert.banner.hint=Możesz ponowić te zmiany, dopóki nie wyślesz nowej wiadomości
revert.message.rollback=Cofnij do tej wiadomości revert.message.rollback=Cofnij do tej wiadomości
# Context settings
settings.context.displayName=Kontekst
settings.context.description=Skonfiguruj zachowanie kontekstu.
settings.context.save.pending=Zapisywanie ustawień kontekstu...
settings.context.save.failed=Nie udało się zapisać ustawień kontekstu
settings.context.compaction.title=Kompaktowanie
settings.context.compaction.auto.title=Automatyczna kompakcja
settings.context.compaction.auto.description=Automatycznie kompaktuj kontekst, zanim osiągnie limit
settings.context.compaction.threshold.title=Limit automatycznego kompaktowania
settings.context.compaction.threshold.description=Kompaktuj, gdy kontekst osiągnie ten procent okna modelu. Pozostaw puste, aby używać tylko bufora bezpieczeństwa.
settings.context.compaction.threshold.placeholder=Domyślne
settings.context.compaction.threshold.suffix=%
settings.context.compaction.threshold.invalid=Wpisz liczbę od 0 do 100 albo pozostaw pole puste.
settings.context.compaction.prune.title=Przytnij stare wyjścia
settings.context.compaction.prune.description=Usuń stare wyjścia narzędzi podczas kompakcji
settings.context.watcher.title=Wzorce ignorowania obserwatora plików
settings.context.watcher.description=Wzorce glob dla plików do ignorowania
settings.context.watcher.add=Dodaj wzorzec
settings.context.watcher.remove=Usuń wybrane wzorce
settings.context.watcher.empty=Nie skonfigurowano wzorców ignorowania.
settings.context.watcher.input.title=Dodaj wzorzec ignorowania
settings.context.watcher.input.prompt=Wpisz wzorzec glob do ignorowania:
@@ -1,4 +1,6 @@
common.delete=Excluir common.delete=Excluir
common.open=Abrir
common.save=Salvar
session.action.cancel=Cancelar session.action.cancel=Cancelar
session.connection.connecting=Carregando… session.connection.connecting=Carregando…
session.connection.error.app=Falha na conexão 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.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.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. 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.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
revert.banner.count.one={0} mensagem revertida revert.banner.count.one={0} mensagem revertida
@@ -376,3 +403,26 @@ revert.banner.redo=Refazer
revert.banner.redo.all=Refazer tudo revert.banner.redo.all=Refazer tudo
revert.banner.hint=Você pode refazer essas alterações até enviar uma nova mensagem revert.banner.hint=Você pode refazer essas alterações até enviar uma nova mensagem
revert.message.rollback=Reverter para esta mensagem revert.message.rollback=Reverter para esta mensagem
# Context settings
settings.context.displayName=Contexto
settings.context.description=Configure o comportamento do contexto.
settings.context.save.pending=Salvando configurações de contexto...
settings.context.save.failed=Falha ao salvar configurações de contexto
settings.context.compaction.title=Compactação
settings.context.compaction.auto.title=Compactação automática
settings.context.compaction.auto.description=Compactar automaticamente o contexto antes que atinja o limite
settings.context.compaction.threshold.title=Limite de compactação automática
settings.context.compaction.threshold.description=Compacte quando o contexto atingir esta porcentagem da janela do modelo. Deixe em branco para usar apenas a margem de segurança.
settings.context.compaction.threshold.placeholder=Padrão
settings.context.compaction.threshold.suffix=%
settings.context.compaction.threshold.invalid=Digite um número de 0 a 100 ou deixe o campo em branco.
settings.context.compaction.prune.title=Remover saídas antigas
settings.context.compaction.prune.description=Remover saídas antigas de ferramentas durante a compactação
settings.context.watcher.title=Padrões de ignorar do observador
settings.context.watcher.description=Padrões glob para arquivos que o observador deve ignorar
settings.context.watcher.add=Adicionar padrão
settings.context.watcher.remove=Remover padrões selecionados
settings.context.watcher.empty=Nenhum padrão de ignorar configurado.
settings.context.watcher.input.title=Adicionar padrão de ignorar
settings.context.watcher.input.prompt=Digite um padrão glob para ignorar:
@@ -1,4 +1,6 @@
common.delete=Удалить common.delete=Удалить
common.open=Открыть
common.save=Сохранить
session.action.cancel=Отмена session.action.cancel=Отмена
session.connection.connecting=Загрузка… session.connection.connecting=Загрузка…
session.connection.error.app=Ошибка подключения session.connection.error.app=Ошибка подключения
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.agents.create.failed=Could not create the agent.
settings.agentBehavior.mcp.addHint=Чтобы добавить MCP-сервер, попросите агента сделать это. 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. 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.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
revert.banner.count.one=Отменено сообщений: {0} revert.banner.count.one=Отменено сообщений: {0}
@@ -376,3 +403,26 @@ revert.banner.redo=Повторить
revert.banner.redo.all=Повторить все revert.banner.redo.all=Повторить все
revert.banner.hint=Эти изменения можно повторить до отправки нового сообщения revert.banner.hint=Эти изменения можно повторить до отправки нового сообщения
revert.message.rollback=Откатиться к этому сообщению revert.message.rollback=Откатиться к этому сообщению
# Context settings
settings.context.displayName=Контекст
settings.context.description=Настройте поведение контекста.
settings.context.save.pending=Сохранение настроек контекста...
settings.context.save.failed=Не удалось сохранить настройки контекста
settings.context.compaction.title=Сжатие
settings.context.compaction.auto.title=Автоматическое сжатие
settings.context.compaction.auto.description=Автоматически сжимать контекст до достижения лимита
settings.context.compaction.threshold.title=Лимит автоматического сжатия
settings.context.compaction.threshold.description=Сжимать, когда контекст достигает этого процента окна модели. Оставьте пустым, чтобы использовать только буфер безопасности.
settings.context.compaction.threshold.placeholder=По умолчанию
settings.context.compaction.threshold.suffix=%
settings.context.compaction.threshold.invalid=Введите число от 0 до 100 или оставьте поле пустым.
settings.context.compaction.prune.title=Очистить старые выходные данные
settings.context.compaction.prune.description=Удалить старые выходные данные инструментов при сжатии
settings.context.watcher.title=Шаблоны игнорирования наблюдателя файлов
settings.context.watcher.description=Glob-шаблоны для файлов, которые наблюдатель должен игнорировать
settings.context.watcher.add=Добавить шаблон
settings.context.watcher.remove=Удалить выбранные шаблоны
settings.context.watcher.empty=Шаблоны игнорирования не настроены.
settings.context.watcher.input.title=Добавить шаблон игнорирования
settings.context.watcher.input.prompt=Введите glob-шаблон для игнорирования:
@@ -1,4 +1,6 @@
common.delete=ลบ common.delete=ลบ
common.open=เปิด
common.save=บันทึก
session.action.cancel=ยกเลิก session.action.cancel=ยกเลิก
session.connection.connecting=กำลังโหลด… session.connection.connecting=กำลังโหลด…
session.connection.error.app=การเชื่อมต่อล้มเหลว session.connection.error.app=การเชื่อมต่อล้มเหลว
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.agents.create.failed=Could not create the agent.
settings.agentBehavior.mcp.addHint=หากต้องการเพิ่มเซิร์ฟเวอร์ MCP ให้ขอให้เอเจนต์เพิ่มให้ 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. 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.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
revert.banner.count.one=ย้อนกลับข้อความ {0} รายการแล้ว revert.banner.count.one=ย้อนกลับข้อความ {0} รายการแล้ว
@@ -376,3 +403,26 @@ revert.banner.redo=ทำซ้ำ
revert.banner.redo.all=ทำซ้ำทั้งหมด revert.banner.redo.all=ทำซ้ำทั้งหมด
revert.banner.hint=คุณสามารถทำซ้ำการเปลี่ยนแปลงเหล่านี้ได้จนกว่าจะส่งข้อความใหม่ revert.banner.hint=คุณสามารถทำซ้ำการเปลี่ยนแปลงเหล่านี้ได้จนกว่าจะส่งข้อความใหม่
revert.message.rollback=ย้อนกลับไปยังข้อความนี้ revert.message.rollback=ย้อนกลับไปยังข้อความนี้
# Context settings
settings.context.displayName=บริบท
settings.context.description=กำหนดค่าพฤติกรรมของบริบท
settings.context.save.pending=กำลังบันทึกการตั้งค่าบริบท...
settings.context.save.failed=บันทึกการตั้งค่าบริบทไม่สำเร็จ
settings.context.compaction.title=การบีบอัด
settings.context.compaction.auto.title=การบีบอัดอัตโนมัติ
settings.context.compaction.auto.description=บีบอัดบริบทอัตโนมัติก่อนถึงขีดจำกัด
settings.context.compaction.threshold.title=ขีดจำกัดการบีบอัดอัตโนมัติ
settings.context.compaction.threshold.description=บีบอัดเมื่อบริบทถึงเปอร์เซ็นต์นี้ของหน้าต่างโมเดล เว้นว่างไว้เพื่อใช้เฉพาะบัฟเฟอร์ความปลอดภัย
settings.context.compaction.threshold.placeholder=ค่าเริ่มต้น
settings.context.compaction.threshold.suffix=%
settings.context.compaction.threshold.invalid=ป้อนตัวเลขตั้งแต่ 0 ถึง 100 หรือเว้นช่องว่างไว้
settings.context.compaction.prune.title=ตัดผลลัพธ์เก่า
settings.context.compaction.prune.description=ลบผลลัพธ์เครื่องมือเก่าระหว่างการบีบอัด
settings.context.watcher.title=รูปแบบการละเว้นตัวเฝ้าดูไฟล์
settings.context.watcher.description=รูปแบบ glob สำหรับไฟล์ที่ตัวเฝ้าดูควรละเว้น
settings.context.watcher.add=เพิ่มรูปแบบ
settings.context.watcher.remove=ลบรูปแบบที่เลือก
settings.context.watcher.empty=ยังไม่ได้กำหนดค่ารูปแบบการละเว้น
settings.context.watcher.input.title=เพิ่มรูปแบบการละเว้น
settings.context.watcher.input.prompt=ป้อนรูปแบบ glob ที่จะละเว้น:
@@ -1,4 +1,6 @@
common.delete=Sil common.delete=Sil
common.open=
common.save=Kaydet
session.action.cancel=İptal session.action.cancel=İptal
session.connection.connecting=Yükleniyor… session.connection.connecting=Yükleniyor…
session.connection.error.app=Bağlantı hatası 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.agents.create.failed=Could not create the agent.
settings.agentBehavior.mcp.addHint=MCP sunucusu eklemek için ajandan bunu yapmasını isteyin. 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. 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.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
revert.banner.count.one={0} mesaj geri alındı revert.banner.count.one={0} mesaj geri alındı
@@ -376,3 +403,26 @@ revert.banner.redo=Yinele
revert.banner.redo.all=Tümünü yinele revert.banner.redo.all=Tümünü yinele
revert.banner.hint=Yeni bir mesaj gönderene kadar bu değişiklikleri yineleyebilirsiniz revert.banner.hint=Yeni bir mesaj gönderene kadar bu değişiklikleri yineleyebilirsiniz
revert.message.rollback=Bu mesaja geri dön revert.message.rollback=Bu mesaja geri dön
# Context settings
settings.context.displayName=Bağlam
settings.context.description=Bağlam davranışını yapılandırın.
settings.context.save.pending=Bağlam ayarları kaydediliyor...
settings.context.save.failed=Bağlam ayarları kaydedilemedi
settings.context.compaction.title=Sıkıştırma
settings.context.compaction.auto.title=Otomatik Sıkıştırma
settings.context.compaction.auto.description=Bağlam sınıra ulaşmadan önce otomatik olarak sıkıştır
settings.context.compaction.threshold.title=Otomatik sıkıştırma sınırı
settings.context.compaction.threshold.description=Bağlam model penceresinin bu yüzdesine ulaştığında sıkıştır. Yalnızca güvenlik tamponunu kullanmak için boş bırakın.
settings.context.compaction.threshold.placeholder=Varsayılan
settings.context.compaction.threshold.suffix=%
settings.context.compaction.threshold.invalid=0 ile 100 arasında bir sayı girin veya alanı boş bırakın.
settings.context.compaction.prune.title=Eski Çıktıları Temizle
settings.context.compaction.prune.description=Sıkıştırma sırasında eski araç çıktılarını kaldır
settings.context.watcher.title=Dosya İzleyici Yok Sayma Kalıpları
settings.context.watcher.description=İzleyicinin yok sayması gereken dosyalar için glob kalıpları
settings.context.watcher.add=Kalıp ekle
settings.context.watcher.remove=Seçili kalıpları kaldır
settings.context.watcher.empty=Yok sayma kalıbı yapılandırılmadı.
settings.context.watcher.input.title=Yok sayma kalıbı ekle
settings.context.watcher.input.prompt=Yok sayılacak bir glob kalıbı girin:
@@ -1,4 +1,6 @@
common.delete=Видалити common.delete=Видалити
common.open=Відкрити
common.save=Зберегти
session.action.cancel=Скасувати session.action.cancel=Скасувати
session.connection.connecting=Завантаження… session.connection.connecting=Завантаження…
session.connection.error.app=Помилка з'єднання session.connection.error.app=Помилка з'єднання
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.agents.create.failed=Could not create the agent.
settings.agentBehavior.mcp.addHint=Щоб додати сервер MCP, попросіть агента зробити це. 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. 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.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
revert.banner.count.one=Відкочено повідомлень: {0} revert.banner.count.one=Відкочено повідомлень: {0}
@@ -376,3 +403,26 @@ revert.banner.redo=Повторити
revert.banner.redo.all=Повторити все revert.banner.redo.all=Повторити все
revert.banner.hint=Ці зміни можна повторити, доки ви не надішлете нове повідомлення revert.banner.hint=Ці зміни можна повторити, доки ви не надішлете нове повідомлення
revert.message.rollback=Відкотитися до цього повідомлення revert.message.rollback=Відкотитися до цього повідомлення
# Context settings
settings.context.displayName=Контекст
settings.context.description=Налаштуйте поведінку контексту.
settings.context.save.pending=Збереження налаштувань контексту...
settings.context.save.failed=Не вдалося зберегти налаштування контексту
settings.context.compaction.title=Стискання
settings.context.compaction.auto.title=Автоматичне стиснення
settings.context.compaction.auto.description=Автоматично стискати контекст до досягнення ліміту
settings.context.compaction.threshold.title=Ліміт автоматичного стискання
settings.context.compaction.threshold.description=Стискати, коли контекст досягає цього відсотка вікна моделі. Залиште порожнім, щоб використовувати лише буфер безпеки.
settings.context.compaction.threshold.placeholder=За замовчуванням
settings.context.compaction.threshold.suffix=%
settings.context.compaction.threshold.invalid=Введіть число від 0 до 100 або залиште поле порожнім.
settings.context.compaction.prune.title=Очищати старі виводи
settings.context.compaction.prune.description=Видаляти старі виводи інструментів під час стиснення
settings.context.watcher.title=Шаблони ігнорування спостерігача файлів
settings.context.watcher.description=Glob-шаблони для файлів, які спостерігач має ігнорувати
settings.context.watcher.add=Додати шаблон
settings.context.watcher.remove=Видалити вибрані шаблони
settings.context.watcher.empty=Шаблони ігнорування не налаштовано.
settings.context.watcher.input.title=Додати шаблон ігнорування
settings.context.watcher.input.prompt=Введіть glob-шаблон для ігнорування:
@@ -1,4 +1,6 @@
common.delete=删除 common.delete=删除
common.open=打开
common.save=保存
session.action.cancel=取消 session.action.cancel=取消
session.connection.connecting=加载中… session.connection.connecting=加载中…
session.connection.error.app=连接失败 session.connection.error.app=连接失败
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.agents.create.failed=Could not create the agent.
settings.agentBehavior.mcp.addHint=要添加 MCP 服务器,请让代理为你添加。 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. 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.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
revert.banner.count.one=已回滚 {0} 条消息 revert.banner.count.one=已回滚 {0} 条消息
@@ -376,3 +403,26 @@ revert.banner.redo=重做
revert.banner.redo.all=全部重做 revert.banner.redo.all=全部重做
revert.banner.hint=在发送新消息之前,你可以重做这些更改 revert.banner.hint=在发送新消息之前,你可以重做这些更改
revert.message.rollback=回滚到此消息 revert.message.rollback=回滚到此消息
# Context settings
settings.context.displayName=上下文
settings.context.description=配置上下文行为。
settings.context.save.pending=正在保存上下文设置...
settings.context.save.failed=保存上下文设置失败
settings.context.compaction.title=压缩
settings.context.compaction.auto.title=自动压缩
settings.context.compaction.auto.description=在上下文达到限制前自动压缩
settings.context.compaction.threshold.title=自动压缩限制
settings.context.compaction.threshold.description=当上下文达到模型窗口的此百分比时进行压缩。留空则仅使用安全缓冲区。
settings.context.compaction.threshold.placeholder=默认
settings.context.compaction.threshold.suffix=%
settings.context.compaction.threshold.invalid=输入 0 到 100 之间的数字,或将字段留空。
settings.context.compaction.prune.title=修剪旧输出
settings.context.compaction.prune.description=压缩期间移除旧的工具输出
settings.context.watcher.title=文件监视器忽略模式
settings.context.watcher.description=监视器应忽略的文件的 glob 模式
settings.context.watcher.add=添加模式
settings.context.watcher.remove=移除所选模式
settings.context.watcher.empty=未配置忽略模式。
settings.context.watcher.input.title=添加忽略模式
settings.context.watcher.input.prompt=输入要忽略的 glob 模式:
@@ -1,4 +1,6 @@
common.delete=刪除 common.delete=刪除
common.open=開啟
common.save=儲存
session.action.cancel=取消 session.action.cancel=取消
session.connection.connecting=載入中… session.connection.connecting=載入中…
session.connection.error.app=連線失敗 session.connection.error.app=連線失敗
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.agents.create.failed=Could not create the agent.
settings.agentBehavior.mcp.addHint=若要新增 MCP 伺服器,請請代理為你新增。 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. 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.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
revert.banner.count.one=已回復 {0} 則訊息 revert.banner.count.one=已回復 {0} 則訊息
@@ -376,3 +403,26 @@ revert.banner.redo=重做
revert.banner.redo.all=全部重做 revert.banner.redo.all=全部重做
revert.banner.hint=在傳送新訊息之前,你可以重做這些變更 revert.banner.hint=在傳送新訊息之前,你可以重做這些變更
revert.message.rollback=回復到此訊息 revert.message.rollback=回復到此訊息
# Context settings
settings.context.displayName=上下文
settings.context.description=設定上下文行為。
settings.context.save.pending=正在儲存上下文設定...
settings.context.save.failed=無法儲存上下文設定
settings.context.compaction.title=壓縮
settings.context.compaction.auto.title=自動壓縮
settings.context.compaction.auto.description=在上下文達到限制前自動壓縮
settings.context.compaction.threshold.title=自動壓縮限制
settings.context.compaction.threshold.description=當上下文達到模型視窗的此百分比時進行壓縮。留空則僅使用安全緩衝區。
settings.context.compaction.threshold.placeholder=預設
settings.context.compaction.threshold.suffix=%
settings.context.compaction.threshold.invalid=輸入 0 到 100 之間的數字,或將欄位留空。
settings.context.compaction.prune.title=修剪舊輸出
settings.context.compaction.prune.description=壓縮期間移除舊的工具輸出
settings.context.watcher.title=檔案監視器忽略模式
settings.context.watcher.description=監視器應忽略的檔案的 glob 模式
settings.context.watcher.add=新增模式
settings.context.watcher.remove=移除所選模式
settings.context.watcher.empty=尚未設定忽略模式。
settings.context.watcher.input.title=新增忽略模式
settings.context.watcher.input.prompt=輸入要忽略的 glob 模式:
@@ -3,6 +3,7 @@ package ai.kilocode.client.app
import ai.kilocode.client.testing.FakeAgentBehaviorRpcApi import ai.kilocode.client.testing.FakeAgentBehaviorRpcApi
import ai.kilocode.rpc.dto.AgentCreateDto import ai.kilocode.rpc.dto.AgentCreateDto
import ai.kilocode.rpc.dto.McpStatusDto import ai.kilocode.rpc.dto.McpStatusDto
import ai.kilocode.rpc.dto.SkillDto
import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.testFramework.fixtures.BasePlatformTestCase
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -10,6 +11,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.test.assertFailsWith
class KiloAgentBehaviorServiceTest : BasePlatformTestCase() { class KiloAgentBehaviorServiceTest : BasePlatformTestCase() {
private lateinit var scope: CoroutineScope private lateinit var scope: CoroutineScope
@@ -67,6 +69,35 @@ class KiloAgentBehaviorServiceTest : BasePlatformTestCase() {
assertTrue(rpc.removals.isEmpty()) 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 { fun `test mcpStatus forwards directory`() = runBlocking {
rpc.mcps = listOf(McpStatusDto("filesystem", "connected")) rpc.mcps = listOf(McpStatusDto("filesystem", "connected"))
@@ -97,4 +97,12 @@ class KiloWorkspaceServiceTest : BasePlatformTestCase() {
assertEquals(err.message, seen?.message) assertEquals(err.message, seen?.message)
assertEquals(listOf("dep"), rpc.searchQueries) assertEquals(listOf("dep"), rpc.searchQueries)
} }
fun `test searchFiles sends query to RPC`() = runBlocking {
withContext(Dispatchers.Default) {
service.searchFiles("/test", "src")
}
assertEquals(listOf("src"), rpc.searchQueries)
}
} }
@@ -44,7 +44,6 @@ import com.intellij.openapi.actionSystem.UiDataProvider
import com.intellij.openapi.actionSystem.ex.ActionUtil import com.intellij.openapi.actionSystem.ex.ActionUtil
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.command.undo.UndoManager
import com.intellij.openapi.editor.DefaultLanguageHighlighterColors import com.intellij.openapi.editor.DefaultLanguageHighlighterColors
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.EditorFactory import com.intellij.openapi.editor.EditorFactory
@@ -56,7 +55,6 @@ import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.openapi.editor.colors.EditorColorsScheme import com.intellij.openapi.editor.colors.EditorColorsScheme
import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.editor.markup.TextAttributes import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.ide.CopyPasteManager import com.intellij.openapi.ide.CopyPasteManager
import com.intellij.openapi.fileTypes.PlainTextFileType import com.intellij.openapi.fileTypes.PlainTextFileType
import com.intellij.openapi.fileTypes.PlainTextLanguage import com.intellij.openapi.fileTypes.PlainTextLanguage
@@ -455,28 +453,20 @@ class PromptPanelTest : BasePlatformTestCase() {
assertTrue(spans.contains("@unknown" to CodeInsightColors.WRONG_REFERENCES_ATTRIBUTES)) assertTrue(spans.contains("@unknown" to CodeInsightColors.WRONG_REFERENCES_ATTRIBUTES))
} }
fun `test prompt editor exposes file editor for undo redo`() { fun `test prompt editor does not expose file editor for platform undo`() {
val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> }, completion = completion()) val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> }, completion = completion())
val field = panel.defaultFocusedComponent as EditorTextField val field = panel.defaultFocusedComponent as EditorTextField
realize(panel, 260, 400) realize(panel, 260, 400)
val editor = field.getEditor(false)!! val editor = field.getEditor(false)!!
WriteCommandAction.runWriteCommandAction(project) {
editor.document.insertString(0, "hello")
}
val sink = TestSink() val sink = TestSink()
(field as UiDataProvider).uiDataSnapshot(sink) (field as UiDataProvider).uiDataSnapshot(sink)
val file = sink.file as? TextEditor ?: error("missing file editor")
assertNotNull(file) assertNull(sink.file)
assertSame(editor.document, file.editor.document) assertSame(true, editor.getUserData(EditorTextField.SUPPLEMENTARY_KEY))
UndoManager.getInstance(project).undo(file)
assertEquals("", editor.document.text)
UndoManager.getInstance(project).redo(file)
assertEquals("hello", editor.document.text)
} }
fun `test prompt editor platform undo redo actions target prompt editor`() { fun `test prompt editor platform undo redo actions do not throw`() {
val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> }, completion = completion()) val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> }, completion = completion())
val field = panel.defaultFocusedComponent as EditorTextField val field = panel.defaultFocusedComponent as EditorTextField
@@ -488,13 +478,10 @@ class PromptPanelTest : BasePlatformTestCase() {
assertSame(true, editor.contentComponent.getClientProperty(UndoRedoAction.IGNORE_SWING_UNDO_MANAGER)) assertSame(true, editor.contentComponent.getClientProperty(UndoRedoAction.IGNORE_SWING_UNDO_MANAGER))
val sink = TestSink() val sink = TestSink()
(field as UiDataProvider).uiDataSnapshot(sink) (field as UiDataProvider).uiDataSnapshot(sink)
val file = sink.file as? TextEditor ?: error("missing file editor") assertNull(sink.file)
assertSame(editor.document, file.editor.document)
assertTrue("prompt file editor should have undo", UndoManager.getInstance(project).isUndoAvailable(file))
invokeAction(IdeActions.ACTION_UNDO, editor.contentComponent, file) updatePlatformAction(IdeActions.ACTION_UNDO, editor)
assertEquals("", editor.document.text) updatePlatformAction(IdeActions.ACTION_REDO, editor)
invokeAction(IdeActions.ACTION_REDO, editor.contentComponent, file)
assertEquals("hello", editor.document.text) assertEquals("hello", editor.document.text)
} }
@@ -1364,21 +1351,18 @@ class PromptPanelTest : BasePlatformTestCase() {
UIUtil.dispatchAllInvocationEvents() UIUtil.dispatchAllInvocationEvents()
} }
private fun invokeAction(id: String, component: java.awt.Component, file: TextEditor) { private fun updatePlatformAction(id: String, editor: Editor) {
val action = ActionManager.getInstance().getAction(id) ?: error("missing action $id") val action = ActionManager.getInstance().getAction(id) ?: error("missing action $id")
val ctx = DataContext { data -> val ctx = DataContext { data ->
when (data) { when (data) {
CommonDataKeys.PROJECT.name -> project CommonDataKeys.PROJECT.name -> project
PlatformCoreDataKeys.CONTEXT_COMPONENT.name -> component CommonDataKeys.EDITOR.name -> editor
PlatformCoreDataKeys.FILE_EDITOR.name -> file PlatformCoreDataKeys.CONTEXT_COMPONENT.name -> editor.contentComponent
else -> null else -> null
} }
} }
val event = AnActionEvent.createEvent(action, ctx, null, ActionPlaces.UNKNOWN, ActionUiKind.NONE, null) val event = AnActionEvent.createEvent(action, ctx, null, ActionPlaces.UNKNOWN, ActionUiKind.NONE, null)
ActionUtil.updateAction(action, event) ActionUtil.updateAction(action, event)
assertTrue("action $id should be enabled", event.presentation.isEnabled)
ActionUtil.performAction(action, event)
UIUtil.dispatchAllInvocationEvents()
} }
private fun waitForLookupItems(editor: Editor): List<String> { private fun waitForLookupItems(editor: Editor): List<String> {
@@ -1,6 +1,7 @@
package ai.kilocode.client.settings package ai.kilocode.client.settings
import ai.kilocode.client.settings.profile.UserProfileConfigurable import ai.kilocode.client.settings.profile.UserProfileConfigurable
import ai.kilocode.client.settings.context.ContextConfigurable
import ai.kilocode.client.settings.models.ModelsConfigurable import ai.kilocode.client.settings.models.ModelsConfigurable
import ai.kilocode.client.settings.agents.AgentBehaviorConfigurable import ai.kilocode.client.settings.agents.AgentBehaviorConfigurable
import ai.kilocode.client.settings.providers.ProvidersConfigurable import ai.kilocode.client.settings.providers.ProvidersConfigurable
@@ -29,6 +30,10 @@ class KiloSettingsConfigurableTest : BasePlatformTestCase() {
assertEquals("ai.kilocode.jetbrains.settings.models", ModelsConfigurable.ID) assertEquals("ai.kilocode.jetbrains.settings.models", ModelsConfigurable.ID)
} }
fun `test child context id matches xml registration`() {
assertEquals("ai.kilocode.jetbrains.settings.context", ContextConfigurable.ID)
}
fun `test child provider and behavior ids match xml registration`() { fun `test child provider and behavior ids match xml registration`() {
assertEquals("ai.kilocode.jetbrains.settings.providers", ProvidersConfigurable.ID) assertEquals("ai.kilocode.jetbrains.settings.providers", ProvidersConfigurable.ID)
assertEquals("ai.kilocode.jetbrains.settings.agentBehavior", AgentBehaviorConfigurable.ID) assertEquals("ai.kilocode.jetbrains.settings.agentBehavior", AgentBehaviorConfigurable.ID)
@@ -79,12 +84,21 @@ class KiloSettingsConfigurableTest : BasePlatformTestCase() {
} }
} }
fun `test createComponent contains Context link`() {
val cfg = KiloSettingsConfigurable()
edt {
val panel = cfg.createComponent()
val links = links(panel as Container)
assertTrue("expected a link labeled 'Context'", links.any { it.text == "Context" })
}
}
fun `test createComponent contains settings links in order`() { fun `test createComponent contains settings links in order`() {
val cfg = KiloSettingsConfigurable() val cfg = KiloSettingsConfigurable()
edt { edt {
val panel = cfg.createComponent() val panel = cfg.createComponent()
val labels = links(panel as Container).map { it.text } val labels = links(panel as Container).map { it.text }
assertEquals(listOf("User Profile", "Models", "Providers", "Agent Behavior"), labels) assertEquals(listOf("User Profile", "Models", "Providers", "Agent Behavior", "Context"), labels)
} }
} }
@@ -18,6 +18,7 @@ class AgentBehaviorConfigurableTest : BasePlatformTestCase() {
fun `test child ids match xml registration`() { fun `test child ids match xml registration`() {
assertEquals("ai.kilocode.jetbrains.settings.agentBehavior.agents", AgentsConfigurable.ID) assertEquals("ai.kilocode.jetbrains.settings.agentBehavior.agents", AgentsConfigurable.ID)
assertEquals("ai.kilocode.jetbrains.settings.agentBehavior.mcp", McpConfigurable.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`() { fun `test createComponent contains child links in order`() {
@@ -26,7 +27,7 @@ class AgentBehaviorConfigurableTest : BasePlatformTestCase() {
edt { edt {
val panel = cfg.createComponent() val panel = cfg.createComponent()
val labels = links(panel as Container).map { it.text } val labels = links(panel as Container).map { it.text }
assertEquals(listOf("Agents", "MCP Servers"), labels) assertEquals(listOf("Agents", "MCP Servers", "Skills"), labels)
} }
} }
@@ -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
}
@@ -88,6 +88,66 @@ class SettingsDraftStateTest {
assertFalse(state.modified()) assertFalse(state.modified())
} }
@Test
fun `stale external base after fallback completion does not revert applied target`() {
val state = SettingsDraftState("old")
state.update { "new" }
val token = state.start()!!
state.complete(token, "old")
state.accept("old")
assertEquals("new", state.baseline)
assertEquals("new", state.draft)
assertFalse(state.modified())
}
@Test
fun `stale external base after fresh completion does not revert applied target`() {
val state = SettingsDraftState("old")
state.update { "new" }
val token = state.start()!!
state.complete(token, "new")
state.accept("old")
assertEquals("new", state.baseline)
assertEquals("new", state.draft)
assertFalse(state.modified())
}
@Test
fun `fresh external base after ignored stale update is accepted`() {
val state = SettingsDraftState("old")
state.update { "new" }
val token = state.start()!!
state.complete(token, "old")
state.accept("old")
state.accept("other")
assertEquals("other", state.baseline)
assertEquals("other", state.draft)
assertFalse(state.modified())
}
@Test
fun `older stale external base after multiple saves is ignored`() {
val state = SettingsDraftState("old")
state.update { "new" }
val first = state.start()!!
state.complete(first, "new")
state.update { "other" }
val second = state.start()!!
state.complete(second, "other")
state.accept("old")
assertEquals("other", state.baseline)
assertEquals("other", state.draft)
assertFalse(state.modified())
}
@Test @Test
fun `failed save keeps draft dirty and restores previous base`() { fun `failed save keeps draft dirty and restores previous base`() {
val state = SettingsDraftState("old") val state = SettingsDraftState("old")
@@ -14,6 +14,9 @@ import java.awt.Dimension
import java.awt.Point import java.awt.Point
import java.awt.event.InputEvent import java.awt.event.InputEvent
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
import javax.swing.ListSelectionModel
import javax.swing.Scrollable
import javax.swing.SwingConstants
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
class SettingsListViewTest : BasePlatformTestCase() { 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`() { fun `test update selects preferred key`() {
edt { edt {
val view = SettingsListView("Empty") { _, _ -> } 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 { private fun item(id: String, name: String, note: String?, vararg cells: SettingsListCell) = object : SettingsListItem {
override val key = id override val key = id
override val title = name override val title = name
@@ -0,0 +1,76 @@
package ai.kilocode.client.settings.context
import ai.kilocode.rpc.dto.CompactionConfigDto
import ai.kilocode.rpc.dto.ConfigDto
import ai.kilocode.rpc.dto.WatcherConfigDto
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
class ContextSettingsStateTest {
@Test
fun `draft reads context config`() {
val draft = contextDraft(ConfigDto(
watcher = WatcherConfigDto(ignore = listOf("**/dist/**")),
compaction = CompactionConfigDto(auto = true, threshold_percent = 75.0, prune = true),
))
assertEquals(true, draft.auto)
assertEquals("75", draft.threshold)
assertEquals(true, draft.prune)
assertEquals(listOf("**/dist/**"), draft.ignore)
}
@Test
fun `unchanged draft emits no patch`() {
val draft = ContextDraft(auto = true, threshold = "75", prune = false, ignore = listOf("tmp/**"))
assertEquals(false, patch(draft, draft)?.let(::changed))
}
@Test
fun `boolean false values are emitted`() {
val from = ContextDraft(auto = true, prune = true)
val to = ContextDraft(auto = false, prune = false)
val patch = patch(from, to)
assertEquals(false, patch?.compaction?.auto)
assertEquals(false, patch?.compaction?.prune)
}
@Test
fun `threshold set and clear use explicit semantics`() {
val from = ContextDraft(threshold = "")
val set = ContextDraft(threshold = "80")
val clear = ContextDraft(threshold = "")
assertEquals(80.0, patch(from, set)?.compaction?.threshold_percent)
assertEquals(listOf("threshold_percent"), patch(set, clear)?.compaction?.clear)
assertNull(patch(set, clear)?.compaction?.threshold_percent)
}
@Test
fun `watcher empty list is emitted`() {
val from = ContextDraft(ignore = listOf("**/dist/**"))
val to = ContextDraft(ignore = emptyList())
assertEquals(emptyList(), patch(from, to)?.watcher?.ignore)
}
@Test
fun `invalid threshold prevents patch without looking like no changes`() {
val from = ContextDraft(threshold = "50")
val to = ContextDraft(auto = true, threshold = "101", prune = true, ignore = listOf("tmp/**"))
assertEquals(ThresholdStatus.INVALID, thresholdStatus(to.threshold))
assertNull(patch(from, to))
}
@Test
fun `saved match normalizes threshold formatting`() {
assertTrue(savedMatches(ContextDraft(threshold = "75"), ContextDraft(threshold = "75.0")))
assertFalse(savedMatches(ContextDraft(threshold = "75"), ContextDraft(threshold = "76")))
}
}
@@ -0,0 +1,324 @@
package ai.kilocode.client.settings.context
import ai.kilocode.client.app.KiloAppService
import ai.kilocode.client.app.KiloWorkspaceService
import ai.kilocode.client.settings.base.SettingsToggle
import ai.kilocode.client.ui.HoverIcon
import ai.kilocode.client.testing.FakeAppRpcApi
import ai.kilocode.client.testing.FakeWorkspaceRpcApi
import ai.kilocode.rpc.dto.CompactionConfigDto
import ai.kilocode.rpc.dto.ConfigDto
import ai.kilocode.rpc.dto.KiloAppStateDto
import ai.kilocode.rpc.dto.KiloAppStatusDto
import ai.kilocode.rpc.dto.WatcherConfigDto
import com.intellij.openapi.application.ApplicationManager
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.intellij.ui.components.JBList
import com.intellij.ui.components.JBTextField
import com.intellij.util.ui.UIUtil
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import java.awt.Container
import java.awt.event.MouseEvent
import javax.swing.AbstractButton
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.ListSelectionModel
import javax.swing.JTextField
import javax.swing.text.AbstractDocument
import javax.swing.text.JTextComponent
class ContextSettingsUiTest : BasePlatformTestCase() {
private lateinit var appScope: CoroutineScope
private lateinit var uiScope: CoroutineScope
private lateinit var rpc: FakeAppRpcApi
private lateinit var workspaceRpc: FakeWorkspaceRpcApi
private lateinit var app: KiloAppService
private lateinit var workspaces: KiloWorkspaceService
private var ui: ContextSettingsUi? = null
override fun setUp() {
super.setUp()
appScope = CoroutineScope(SupervisorJob())
uiScope = CoroutineScope(SupervisorJob())
rpc = FakeAppRpcApi()
workspaceRpc = FakeWorkspaceRpcApi()
app = KiloAppService(appScope, rpc)
workspaces = KiloWorkspaceService(appScope, workspaceRpc)
val state = KiloAppStateDto(
KiloAppStatusDto.READY,
config = ConfigDto(
watcher = WatcherConfigDto(ignore = listOf("tmp/**")),
compaction = CompactionConfigDto(auto = true, threshold_percent = 75.0, prune = true),
),
)
rpc.state.value = state
app._state.value = state
edt { ui = ContextSettingsUi(uiScope, app, workspaces) }
flushUntil { text(requireUi()).contains("Auto Compaction") }
}
override fun tearDown() {
try {
val panel = ui
if (panel != null) edt { panel.dispose() }
ui = null
uiScope.cancel()
appScope.cancel()
} finally {
super.tearDown()
}
}
fun `test toggling compaction sends boolean false values`() {
val panel = requireUi()
edt {
val toggles = components(panel).filterIsInstance<SettingsToggle>()
toggles[0].doClick()
toggles[1].doClick()
panel.applyDraft()
}
flushUntil { rpc.configPatches.isNotEmpty() }
val patch = rpc.configPatches.single()
assertEquals(false, patch.compaction?.auto)
assertEquals(false, patch.compaction?.prune)
}
fun `test editing threshold sends number`() {
val panel = requireUi()
edt {
threshold(panel).text = "80"
panel.applyDraft()
}
flushUntil { rpc.configPatches.isNotEmpty() }
assertEquals(80.0, rpc.configPatches.single().compaction?.threshold_percent)
}
fun `test threshold row shows percent label and rejects out of range values`() {
val panel = requireUi()
edt {
val field = threshold(panel)
assertTrue(text(panel).contains("%"))
assertTrue(text(panel).contains("Auto Compaction Limit"))
assertTrue(text(panel).contains("Prune Old Outputs"))
assertEquals("Default", field.emptyText.text)
field.text = ""
field.text = "101"
assertEquals("", field.text)
field.text = "100"
assertEquals("100", field.text)
(field.document as AbstractDocument).replace(0, field.document.length, "-1", null)
assertEquals("100", field.text)
}
}
fun `test clearing threshold sends clear patch`() {
val panel = requireUi()
edt {
threshold(panel).text = ""
panel.applyDraft()
}
flushUntil { rpc.configPatches.isNotEmpty() }
assertEquals(listOf("threshold_percent"), rpc.configPatches.single().compaction?.clear)
}
fun `test adding watcher pattern sends full list`() {
val panel = requireUi()
edt {
val patterns = components(panel).filterIsInstance<PatternList>().single()
patterns.input = { "**/dist/**" }
icon(panel, "Add pattern").doClick()
assertEquals(listOf("**/dist/**"), patternList(panel).selectedValuesList)
panel.applyDraft()
}
flushUntil { rpc.configPatches.isNotEmpty() }
assertEquals(listOf("tmp/**", "**/dist/**"), rpc.configPatches.single().watcher?.ignore)
}
fun `test stale config update result keeps watcher pattern visible`() {
val panel = requireUi()
rpc.configUpdateReturnStale = true
edt {
val patterns = components(panel).filterIsInstance<PatternList>().single()
patterns.input = { "**/dist/**" }
icon(panel, "Add pattern").doClick()
panel.applyDraft()
}
flushUntil { rpc.configPatches.isNotEmpty() && !edt { panel.modified() } }
edt {
val list = patternList(panel)
assertEquals(listOf("**/dist/**"), list.selectedValuesList)
assertEquals(listOf("tmp/**", "**/dist/**"), (0 until list.model.size).map { list.model.getElementAt(it) })
}
}
fun `test removing selected watcher patterns supports multi selection`() {
val panel = requireUi()
edt {
val patterns = components(panel).filterIsInstance<PatternList>().single()
val inputs = ArrayDeque(listOf("**/dist/**", "**/build/**"))
patterns.input = { inputs.removeFirst() }
icon(panel, "Add pattern").doClick()
icon(panel, "Add pattern").doClick()
val list = patternList(panel)
assertEquals(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION, list.selectionMode)
list.setSelectionInterval(0, 1)
icon(panel, "Remove selected patterns").doClick()
panel.applyDraft()
}
flushUntil { rpc.configPatches.isNotEmpty() }
assertEquals(listOf("**/build/**"), rpc.configPatches.single().watcher?.ignore)
}
fun `test double clicking watcher pattern edits it`() {
val panel = requireUi()
edt {
val patterns = components(panel).filterIsInstance<PatternList>().single()
patterns.editor = { "**/edited/**" }
val list = patternList(panel)
list.setSize(400, 100)
list.doLayout()
val bounds = list.getCellBounds(0, 0)
val event = MouseEvent(
list,
MouseEvent.MOUSE_CLICKED,
System.currentTimeMillis(),
0,
bounds.x + 1,
bounds.y + 1,
2,
false,
MouseEvent.BUTTON1,
)
list.mouseListeners.forEach { it.mouseClicked(event) }
assertEquals(listOf("**/edited/**"), list.selectedValuesList)
panel.applyDraft()
}
flushUntil { rpc.configPatches.isNotEmpty() }
assertEquals(listOf("**/edited/**"), rpc.configPatches.single().watcher?.ignore)
}
fun `test watcher pattern renderer has left inset`() {
val panel = requireUi()
edt {
val list = patternList(panel)
val comp = list.cellRenderer.getListCellRendererComponent(list, "tmp/**", 0, false, false) as JComponent
assertTrue(comp.insets.left > 0)
}
}
fun `test watcher section does not repeat ignored patterns row title`() {
val panel = requireUi()
edt {
assertFalse(text(panel).contains("Ignored patterns"))
assertTrue(text(panel).contains("File Watcher Ignore Patterns"))
assertEquals(1, components(panel).filterIsInstance<JTextField>().size)
}
}
fun `test failed apply stays visible while panel open`() {
val panel = requireUi()
rpc.configUpdateError = RuntimeException("save failed")
edt {
threshold(panel).text = "80"
panel.applyDraft()
}
flushUntil { text(panel).contains("Failed to save context settings") }
edt {
assertTrue(text(panel.progress).contains("Failed to save context settings"))
assertTrue(panel.modified())
}
}
fun `test controls are disabled during pending save`() {
val panel = requireUi()
rpc.configUpdateGate = CompletableDeferred()
edt {
threshold(panel).text = "80"
panel.applyDraft()
assertTrue(components(panel).filterIsInstance<SettingsToggle>().all { !it.isEnabled })
assertFalse(threshold(panel).isEnabled)
}
rpc.configUpdateGate?.complete(Unit)
flushUntil { rpc.configPatches.isNotEmpty() }
}
private fun requireUi(): ContextSettingsUi = requireNotNull(ui)
private fun threshold(panel: ContextSettingsUi): JBTextField = components(panel)
.filterIsInstance<JBTextField>()
.single { it.columns == 8 }
private fun patternList(panel: ContextSettingsUi): JBList<String> {
val list = components(panel).filterIsInstance<JBList<*>>().single()
@Suppress("UNCHECKED_CAST")
return list as JBList<String>
}
private fun icon(panel: ContextSettingsUi, tip: String): HoverIcon = components(panel)
.filterIsInstance<HoverIcon>()
.single { it.toolTipText == tip }
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(200) {
delay(10)
edt { UIUtil.dispatchAllInvocationEvents() }
if (done()) return@runBlocking
}
edt { UIUtil.dispatchAllInvocationEvents() }
assertTrue(done())
}
private fun text(root: Container): String {
val out = mutableListOf<String>()
for (comp in components(root)) {
if (!comp.isVisible) continue
when (comp) {
is AbstractButton -> comp.text?.let { out.add(it) }
is JLabel -> comp.text?.let { out.add(it) }
is JTextComponent -> comp.text?.let { out.add(it) }
}
}
return out.joinToString("\n")
}
private fun components(root: Container): List<java.awt.Component> = buildList {
fun visit(comp: java.awt.Component) {
add(comp)
if (comp is Container) comp.components.forEach { visit(it) }
}
visit(root)
}
}
@@ -11,9 +11,14 @@ import ai.kilocode.rpc.dto.SkillDto
class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi { class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi {
var agents = emptyList<AgentDetailDto>() var agents = emptyList<AgentDetailDto>()
var skills = emptyList<SkillDto>()
var mcps = emptyList<McpStatusDto>() var mcps = emptyList<McpStatusDto>()
var mcpConfigs = emptyMap<String, McpServerConfigDto>() var mcpConfigs = emptyMap<String, McpServerConfigDto>()
val agentCalls = mutableListOf<String>() 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 mcpCalls = mutableListOf<String>()
val mcpConfigCalls = mutableListOf<String>() val mcpConfigCalls = mutableListOf<String>()
val mcpSaves = mutableListOf<Triple<String, String, McpConfigDto?>>() val mcpSaves = mutableListOf<Triple<String, String, McpConfigDto?>>()
@@ -27,10 +32,16 @@ class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi {
var afterRemove: (suspend (String, String) -> Unit)? = null var afterRemove: (suspend (String, String) -> Unit)? = null
var afterMcpConnect: (suspend (String, String) -> Unit)? = null var afterMcpConnect: (suspend (String, String) -> Unit)? = null
var createError: Exception? = null var createError: Exception? = null
var skillsError: Exception? = null
var removeError: Exception? = null var removeError: Exception? = null
var removeSkillError: Exception? = null
var saveSkillError: Exception? = null
var mcpStatusError: Exception? = null var mcpStatusError: Exception? = null
var mcpConnectError: Exception? = null var mcpConnectError: Exception? = null
var removeResult = true var removeResult = true
var removeSkillResult = true
var reloadSkillResult = true
var saveSkillResult = true
var mcpConnectResult = true var mcpConnectResult = true
var mcpDisconnectResult = true var mcpDisconnectResult = true
var mcpAuthenticateResult = true var mcpAuthenticateResult = true
@@ -43,12 +54,41 @@ class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi {
override suspend fun skills(directory: String): List<SkillDto> { override suspend fun skills(directory: String): List<SkillDto> {
assertNotEdt("agentBehavior.skills") assertNotEdt("agentBehavior.skills")
return emptyList() skillsError?.let { throw it }
skillCalls.add(directory)
return skills
} }
override suspend fun removeSkill(directory: String, location: String): Boolean { override suspend fun removeSkill(directory: String, location: String): Boolean {
assertNotEdt("agentBehavior.removeSkill") 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 { override suspend fun removeAgent(directory: String, name: String): Boolean {
@@ -2,6 +2,7 @@ package ai.kilocode.client.testing
import ai.kilocode.rpc.KiloAppRpcApi import ai.kilocode.rpc.KiloAppRpcApi
import ai.kilocode.rpc.dto.AgentConfigDto import ai.kilocode.rpc.dto.AgentConfigDto
import ai.kilocode.rpc.dto.CompactionConfigDto
import ai.kilocode.rpc.dto.ConfigDto import ai.kilocode.rpc.dto.ConfigDto
import ai.kilocode.rpc.dto.ConfigPatchDto import ai.kilocode.rpc.dto.ConfigPatchDto
import ai.kilocode.rpc.dto.DeviceAuthDto import ai.kilocode.rpc.dto.DeviceAuthDto
@@ -16,6 +17,7 @@ import ai.kilocode.rpc.dto.ModelVariantUpdateDto
import ai.kilocode.rpc.dto.ProfileDto import ai.kilocode.rpc.dto.ProfileDto
import ai.kilocode.rpc.dto.SkillsConfigDto import ai.kilocode.rpc.dto.SkillsConfigDto
import ai.kilocode.rpc.dto.TelemetryCaptureDto import ai.kilocode.rpc.dto.TelemetryCaptureDto
import ai.kilocode.rpc.dto.WatcherConfigDto
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -196,12 +198,34 @@ class FakeAppRpcApi : KiloAppRpcApi {
val mcp = patch.mcp?.entries?.fold(config.mcp) { acc, (name, item) -> val mcp = patch.mcp?.entries?.fold(config.mcp) { acc, (name, item) ->
if (item == null) acc - name else acc + (name to item) if (item == null) acc - name else acc + (name to item)
} ?: config.mcp } ?: config.mcp
val watcher = patch.watcher?.let { item ->
val cfg = config.watcher
cfg?.copy(ignore = item.ignore ?: cfg.ignore)
?: WatcherConfigDto(ignore = item.ignore ?: emptyList())
} ?: config.watcher
val compaction = patch.compaction?.let { item ->
val cfg = item.clear.fold(config.compaction ?: CompactionConfigDto()) { next, field ->
when (field) {
"threshold_percent" -> next.copy(threshold_percent = null)
"auto" -> next.copy(auto = null)
"prune" -> next.copy(prune = null)
else -> next
}
}
cfg.copy(
auto = item.auto ?: cfg.auto,
threshold_percent = item.threshold_percent ?: cfg.threshold_percent,
prune = item.prune ?: cfg.prune,
)
} ?: config.compaction
return config.copy( return config.copy(
defaultAgent = if (values.containsKey("default_agent")) values["default_agent"] else config.defaultAgent, defaultAgent = if (values.containsKey("default_agent")) values["default_agent"] else config.defaultAgent,
model = if (values.containsKey("model")) values["model"] else config.model, model = if (values.containsKey("model")) values["model"] else config.model,
smallModel = if (values.containsKey("small_model")) values["small_model"] else config.smallModel, smallModel = if (values.containsKey("small_model")) values["small_model"] else config.smallModel,
subagentModel = if (values.containsKey("subagent_model")) values["subagent_model"] else config.subagentModel, subagentModel = if (values.containsKey("subagent_model")) values["subagent_model"] else config.subagentModel,
subagentVariant = if (values.containsKey("subagent_variant")) values["subagent_variant"] else config.subagentVariant, subagentVariant = if (values.containsKey("subagent_variant")) values["subagent_variant"] else config.subagentVariant,
watcher = watcher,
compaction = compaction,
instructions = patch.instructions ?: config.instructions, instructions = patch.instructions ?: config.instructions,
skills = patch.skills?.let { SkillsConfigDto(paths = it.paths.orEmpty(), urls = it.urls.orEmpty()) } ?: config.skills, skills = patch.skills?.let { SkillsConfigDto(paths = it.paths.orEmpty(), urls = it.urls.orEmpty()) } ?: config.skills,
mcp = mcp, mcp = mcp,
@@ -26,6 +26,12 @@ interface KiloAgentBehaviorRpcApi : RemoteApi<Unit> {
suspend fun removeSkill(directory: String, location: String): Boolean 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 removeAgent(directory: String, name: String): Boolean
suspend fun createAgent(directory: String, input: AgentCreateDto): Boolean suspend fun createAgent(directory: String, input: AgentCreateDto): Boolean
@@ -48,7 +48,7 @@ interface KiloWorkspaceRpcApi : RemoteApi<Unit> {
/** Resolve [path] to matching files, scoped primarily to [directory]. */ /** Resolve [path] to matching files, scoped primarily to [directory]. */
suspend fun files(directory: String, path: String): List<WorkspaceFileDto> suspend fun files(directory: String, path: String): List<WorkspaceFileDto>
/** Fuzzy file/folder search via the backend IDE index. */ /** Fuzzy file/folder search via Kilo Core. */
suspend fun searchFiles(directory: String, query: String, limit: Int = 50): FileSearchResultDto suspend fun searchFiles(directory: String, query: String, limit: Int = 50): FileSearchResultDto
/** Current uncommitted git changes as a unified diff for @git-changes mentions. */ /** Current uncommitted git changes as a unified diff for @git-changes mentions. */

Some files were not shown because too many files have changed in this diff Show More