Merge pull request #12292 from Kilo-Org/analyze-jetbrains-vscode-setting-parity

feat(jetbrains): add Context settings parity
This commit is contained in:
Kirill Kalishev
2026-07-20 12:27:18 -04:00
committed by GitHub
37 changed files with 2113 additions and 4 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---
Add JetBrains Context settings for compaction and file watcher ignore patterns.
@@ -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.
+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.
@@ -20,6 +20,7 @@ import ai.kilocode.rpc.dto.CommandDto
import ai.kilocode.rpc.dto.ConfigDto
import ai.kilocode.rpc.dto.ConfigPatchDto
import ai.kilocode.rpc.dto.ConfigUpdateDto
import ai.kilocode.rpc.dto.CompactionConfigDto
import ai.kilocode.rpc.dto.CustomModelDto
import ai.kilocode.rpc.dto.CustomProviderConfigDto
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.TokensDto
import ai.kilocode.rpc.dto.ToolRefDto
import ai.kilocode.rpc.dto.WatcherConfigDto
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
@@ -521,6 +523,8 @@ object KiloCliDataParser {
subagentModel = obj.str("subagent_model"),
subagentVariant = obj.str("subagent_variant"),
defaultAgent = obj.str("default_agent"),
watcher = parseWatcherConfig(obj["watcher"].obj()),
compaction = parseCompactionConfig(obj["compaction"].obj()),
instructions = obj["instructions"].arr()
?.mapNotNull { runCatching { it.jsonPrimitive.contentOrNull }.getOrNull() }
?: emptyList(),
@@ -530,6 +534,24 @@ object KiloCliDataParser {
)
}.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? {
if (obj == null) return null
return SkillsConfigDto(
@@ -838,6 +860,24 @@ object KiloCliDataParser {
val instructions = patch.instructions
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
if (skills != null) {
put("skills", buildJsonObject {
@@ -9,7 +9,9 @@ import ai.kilocode.backend.testing.FakeCliServer
import ai.kilocode.backend.testing.MockCliServer
import ai.kilocode.backend.testing.TestLog
import ai.kilocode.rpc.dto.AgentConfigPatchDto
import ai.kilocode.rpc.dto.CompactionPatchDto
import ai.kilocode.rpc.dto.ConfigPatchDto
import ai.kilocode.rpc.dto.WatcherPatchDto
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -222,6 +224,28 @@ class KiloBackendAppServiceTest {
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
fun `ready dto maps model config`() = runBlocking {
mock.config = """{"model":"openai/gpt","agent":{"plan":{"model":"anthropic/claude","variant":"high"}}}"""
@@ -4,6 +4,7 @@ import ai.kilocode.backend.workspace.CommandInfo
import ai.kilocode.backend.workspace.ProviderData
import ai.kilocode.rpc.dto.ChatEventDto
import ai.kilocode.rpc.dto.AgentConfigPatchDto
import ai.kilocode.rpc.dto.CompactionPatchDto
import ai.kilocode.rpc.dto.ConfigDto
import ai.kilocode.rpc.dto.ConfigPatchDto
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.QuestionReplyDto
import ai.kilocode.rpc.dto.SkillsPatchDto
import ai.kilocode.rpc.dto.WatcherPatchDto
import org.junit.jupiter.api.Nested
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -1173,6 +1175,38 @@ class KiloCliDataParserTest {
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
fun `parseConfig - agent overrides and permissions`() {
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
fun `buildConfigPatch - mcp upsert and delete`() {
val patch = ConfigPatchDto(mcp = linkedMapOf(
@@ -2,6 +2,7 @@ package ai.kilocode.client.settings
import ai.kilocode.client.plugin.KiloBundle
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.providers.ProvidersConfigurable
import ai.kilocode.client.settings.profile.UserProfileConfigurable
@@ -73,6 +74,14 @@ class KiloSettingsConfigurable : SearchableConfigurable {
behavior.border = JBUI.Borders.emptyBottom(UiStyle.Gap.sm())
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
}
@@ -11,6 +11,8 @@ internal class SettingsDraftState<D>(
private var base = initial
private var pending: D? = null
private var stale: List<D> = emptyList()
private var applied: D? = null
private var save = false
private var err: String? = null
@@ -29,6 +31,10 @@ internal class SettingsDraftState<D>(
fun accept(next: D) {
val target = pending
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 edit = draft
base = next
@@ -51,12 +57,15 @@ internal class SettingsDraftState<D>(
fun complete(token: SettingsDraftSave<D>, returned: D) {
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
draft = if (saved(edit, token.target)) next else edit
pending = null
save = false
err = null
stale += token.previous
applied = token.target
}
fun fail(token: SettingsDraftSave<D>, message: String) {
@@ -66,6 +75,8 @@ internal class SettingsDraftState<D>(
pending = null
save = false
err = message
stale = emptyList()
applied = null
}
}
@@ -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
parentId="ai.kilocode.jetbrains.settings"
id="ai.kilocode.jetbrains.settings.profile"
groupWeight="4"
groupWeight="5"
instance="ai.kilocode.client.settings.profile.UserProfileConfigurable"
bundle="messages.KiloBundle"
key="settings.profile.displayName"/>
@@ -41,7 +41,7 @@
<applicationConfigurable
parentId="ai.kilocode.jetbrains.settings"
id="ai.kilocode.jetbrains.settings.models"
groupWeight="3"
groupWeight="4"
instance="ai.kilocode.client.settings.models.ModelsConfigurable"
bundle="messages.KiloBundle"
key="settings.models.displayName"/>
@@ -62,6 +62,14 @@
bundle="messages.KiloBundle"
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
parentId="ai.kilocode.jetbrains.settings.agentBehavior"
id="ai.kilocode.jetbrains.settings.agentBehavior.agents"
@@ -303,6 +303,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.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.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.agentBehavior.displayName=Agent Behavior
settings.agentBehavior.description=Configure agents, MCP servers, rules, workflows, and skills.
@@ -376,3 +376,26 @@ revert.banner.redo=إعادة
revert.banner.redo.all=إعادة الكل
revert.banner.hint=يمكنك إعادة هذه التغييرات حتى ترسل رسالة جديدة
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 لتجاهله:
@@ -376,3 +376,26 @@ revert.banner.redo=Ponovi
revert.banner.redo.all=Ponovi sve
revert.banner.hint=Možete ponoviti ove promjene dok ne pošaljete novu 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:
@@ -376,3 +376,26 @@ revert.banner.redo=Gentag
revert.banner.redo.all=Gentag alle
revert.banner.hint=Du kan gentage disse ændringer, indtil du sender en ny 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:
@@ -376,3 +376,26 @@ revert.banner.redo=Wiederholen
revert.banner.redo.all=Alle wiederholen
revert.banner.hint=Du kannst diese Änderungen wiederholen, bis du eine neue Nachricht sendest
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:
@@ -376,3 +376,26 @@ revert.banner.redo=Rehacer
revert.banner.redo.all=Rehacer todo
revert.banner.hint=Puedes rehacer estos cambios hasta que envíes un mensaje nuevo
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:
@@ -376,3 +376,26 @@ revert.banner.redo=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.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 :
@@ -376,3 +376,26 @@ revert.banner.redo=やり直し
revert.banner.redo.all=すべてやり直し
revert.banner.hint=新しいメッセージを送信するまで、これらの変更をやり直せます
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パターンを入力してください:
@@ -376,3 +376,26 @@ revert.banner.redo=다시 실행
revert.banner.redo.all=모두 다시 실행
revert.banner.hint=새 메시지를 보내기 전까지 이 변경 사항을 다시 실행할 수 있습니다
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 패턴을 입력하세요:
@@ -376,3 +376,26 @@ revert.banner.redo=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.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:
@@ -376,3 +376,26 @@ revert.banner.redo=Gjør om
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.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:
@@ -376,3 +376,26 @@ revert.banner.redo=Ponów
revert.banner.redo.all=Ponów wszystko
revert.banner.hint=Możesz ponowić te zmiany, dopóki nie wyślesz nowej 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:
@@ -376,3 +376,26 @@ revert.banner.redo=Refazer
revert.banner.redo.all=Refazer tudo
revert.banner.hint=Você pode refazer essas alterações até enviar uma nova 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:
@@ -376,3 +376,26 @@ revert.banner.redo=Повторить
revert.banner.redo.all=Повторить все
revert.banner.hint=Эти изменения можно повторить до отправки нового сообщения
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-шаблон для игнорирования:
@@ -376,3 +376,26 @@ revert.banner.redo=ทำซ้ำ
revert.banner.redo.all=ทำซ้ำทั้งหมด
revert.banner.hint=คุณสามารถทำซ้ำการเปลี่ยนแปลงเหล่านี้ได้จนกว่าจะส่งข้อความใหม่
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 ที่จะละเว้น:
@@ -376,3 +376,26 @@ revert.banner.redo=Yinele
revert.banner.redo.all=Tümünü yinele
revert.banner.hint=Yeni bir mesaj gönderene kadar bu değişiklikleri yineleyebilirsiniz
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:
@@ -376,3 +376,26 @@ revert.banner.redo=Повторити
revert.banner.redo.all=Повторити все
revert.banner.hint=Ці зміни можна повторити, доки ви не надішлете нове повідомлення
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-шаблон для ігнорування:
@@ -376,3 +376,26 @@ revert.banner.redo=重做
revert.banner.redo.all=全部重做
revert.banner.hint=在发送新消息之前,你可以重做这些更改
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 模式:
@@ -376,3 +376,26 @@ revert.banner.redo=重做
revert.banner.redo.all=全部重做
revert.banner.hint=在傳送新訊息之前,你可以重做這些變更
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,6 +1,7 @@
package ai.kilocode.client.settings
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.agents.AgentBehaviorConfigurable
import ai.kilocode.client.settings.providers.ProvidersConfigurable
@@ -29,6 +30,10 @@ class KiloSettingsConfigurableTest : BasePlatformTestCase() {
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`() {
assertEquals("ai.kilocode.jetbrains.settings.providers", ProvidersConfigurable.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`() {
val cfg = KiloSettingsConfigurable()
edt {
val panel = cfg.createComponent()
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)
}
}
@@ -88,6 +88,66 @@ class SettingsDraftStateTest {
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
fun `failed save keeps draft dirty and restores previous base`() {
val state = SettingsDraftState("old")
@@ -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)
}
}
@@ -2,6 +2,7 @@ package ai.kilocode.client.testing
import ai.kilocode.rpc.KiloAppRpcApi
import ai.kilocode.rpc.dto.AgentConfigDto
import ai.kilocode.rpc.dto.CompactionConfigDto
import ai.kilocode.rpc.dto.ConfigDto
import ai.kilocode.rpc.dto.ConfigPatchDto
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.SkillsConfigDto
import ai.kilocode.rpc.dto.TelemetryCaptureDto
import ai.kilocode.rpc.dto.WatcherConfigDto
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -196,12 +198,34 @@ class FakeAppRpcApi : KiloAppRpcApi {
val mcp = patch.mcp?.entries?.fold(config.mcp) { acc, (name, item) ->
if (item == null) acc - name else acc + (name to item)
} ?: 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(
defaultAgent = if (values.containsKey("default_agent")) values["default_agent"] else config.defaultAgent,
model = if (values.containsKey("model")) values["model"] else config.model,
smallModel = if (values.containsKey("small_model")) values["small_model"] else config.smallModel,
subagentModel = if (values.containsKey("subagent_model")) values["subagent_model"] else config.subagentModel,
subagentVariant = if (values.containsKey("subagent_variant")) values["subagent_variant"] else config.subagentVariant,
watcher = watcher,
compaction = compaction,
instructions = patch.instructions ?: config.instructions,
skills = patch.skills?.let { SkillsConfigDto(paths = it.paths.orEmpty(), urls = it.urls.orEmpty()) } ?: config.skills,
mcp = mcp,
@@ -64,12 +64,26 @@ data class ConfigDto(
val subagentModel: String? = null,
val subagentVariant: String? = null,
val defaultAgent: String? = null,
val watcher: WatcherConfigDto? = null,
val compaction: CompactionConfigDto? = null,
val instructions: List<String> = emptyList(),
val skills: SkillsConfigDto? = null,
val mcp: Map<String, McpConfigDto> = emptyMap(),
val agent: Map<String, AgentConfigDto> = emptyMap(),
)
@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,
)
@Serializable
data class SkillsConfigDto(
val paths: List<String> = emptyList(),
@@ -109,12 +123,27 @@ sealed class PermissionRuleDto {
@Serializable
data class ConfigPatchDto(
val values: Map<String, String?> = emptyMap(),
val watcher: WatcherPatchDto? = null,
val compaction: CompactionPatchDto? = null,
val instructions: List<String>? = null,
val skills: SkillsPatchDto? = null,
val mcp: Map<String, McpConfigDto?>? = null,
val agents: Map<String, AgentConfigPatchDto> = emptyMap(),
)
@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,
)
@Serializable
data class AgentConfigPatchDto(
val clear: List<String> = emptyList(),