mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
feat(jetbrains): add context settings page
This commit is contained in:
@@ -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.
|
||||||
@@ -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.
|
||||||
+40
@@ -20,6 +20,7 @@ import ai.kilocode.rpc.dto.CommandDto
|
|||||||
import ai.kilocode.rpc.dto.ConfigDto
|
import ai.kilocode.rpc.dto.ConfigDto
|
||||||
import ai.kilocode.rpc.dto.ConfigPatchDto
|
import ai.kilocode.rpc.dto.ConfigPatchDto
|
||||||
import ai.kilocode.rpc.dto.ConfigUpdateDto
|
import ai.kilocode.rpc.dto.ConfigUpdateDto
|
||||||
|
import ai.kilocode.rpc.dto.CompactionConfigDto
|
||||||
import ai.kilocode.rpc.dto.CustomModelDto
|
import ai.kilocode.rpc.dto.CustomModelDto
|
||||||
import ai.kilocode.rpc.dto.CustomProviderConfigDto
|
import ai.kilocode.rpc.dto.CustomProviderConfigDto
|
||||||
import ai.kilocode.rpc.dto.CustomProviderSaveDto
|
import ai.kilocode.rpc.dto.CustomProviderSaveDto
|
||||||
@@ -73,6 +74,7 @@ import ai.kilocode.rpc.dto.TodoDto
|
|||||||
import ai.kilocode.rpc.dto.TodoViewDto
|
import ai.kilocode.rpc.dto.TodoViewDto
|
||||||
import ai.kilocode.rpc.dto.TokensDto
|
import ai.kilocode.rpc.dto.TokensDto
|
||||||
import ai.kilocode.rpc.dto.ToolRefDto
|
import ai.kilocode.rpc.dto.ToolRefDto
|
||||||
|
import ai.kilocode.rpc.dto.WatcherConfigDto
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonArray
|
import kotlinx.serialization.json.JsonArray
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
@@ -521,6 +523,8 @@ object KiloCliDataParser {
|
|||||||
subagentModel = obj.str("subagent_model"),
|
subagentModel = obj.str("subagent_model"),
|
||||||
subagentVariant = obj.str("subagent_variant"),
|
subagentVariant = obj.str("subagent_variant"),
|
||||||
defaultAgent = obj.str("default_agent"),
|
defaultAgent = obj.str("default_agent"),
|
||||||
|
watcher = parseWatcherConfig(obj["watcher"].obj()),
|
||||||
|
compaction = parseCompactionConfig(obj["compaction"].obj()),
|
||||||
instructions = obj["instructions"].arr()
|
instructions = obj["instructions"].arr()
|
||||||
?.mapNotNull { runCatching { it.jsonPrimitive.contentOrNull }.getOrNull() }
|
?.mapNotNull { runCatching { it.jsonPrimitive.contentOrNull }.getOrNull() }
|
||||||
?: emptyList(),
|
?: emptyList(),
|
||||||
@@ -530,6 +534,24 @@ object KiloCliDataParser {
|
|||||||
)
|
)
|
||||||
}.getOrDefault(ConfigDto())
|
}.getOrDefault(ConfigDto())
|
||||||
|
|
||||||
|
private fun parseWatcherConfig(obj: JsonObject?): WatcherConfigDto? {
|
||||||
|
if (obj == null) return null
|
||||||
|
return WatcherConfigDto(
|
||||||
|
ignore = obj["ignore"].arr()
|
||||||
|
?.mapNotNull { runCatching { it.jsonPrimitive.contentOrNull }.getOrNull() }
|
||||||
|
?: emptyList(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseCompactionConfig(obj: JsonObject?): CompactionConfigDto? {
|
||||||
|
if (obj == null) return null
|
||||||
|
return CompactionConfigDto(
|
||||||
|
auto = obj.flagOrNull("auto"),
|
||||||
|
threshold_percent = obj.num("threshold_percent"),
|
||||||
|
prune = obj.flagOrNull("prune"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseSkillsConfig(obj: JsonObject?): SkillsConfigDto? {
|
private fun parseSkillsConfig(obj: JsonObject?): SkillsConfigDto? {
|
||||||
if (obj == null) return null
|
if (obj == null) return null
|
||||||
return SkillsConfigDto(
|
return SkillsConfigDto(
|
||||||
@@ -838,6 +860,24 @@ object KiloCliDataParser {
|
|||||||
val instructions = patch.instructions
|
val instructions = patch.instructions
|
||||||
if (instructions != null) put("instructions", JsonArray(instructions.map(::JsonPrimitive)))
|
if (instructions != null) put("instructions", JsonArray(instructions.map(::JsonPrimitive)))
|
||||||
|
|
||||||
|
val watcher = patch.watcher
|
||||||
|
if (watcher != null) {
|
||||||
|
put("watcher", buildJsonObject {
|
||||||
|
val ignore = watcher.ignore
|
||||||
|
if (ignore != null) put("ignore", JsonArray(ignore.map(::JsonPrimitive)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
val compaction = patch.compaction
|
||||||
|
if (compaction != null) {
|
||||||
|
put("compaction", buildJsonObject {
|
||||||
|
for (field in compaction.clear) put(field, JsonNull)
|
||||||
|
if (compaction.auto != null) put("auto", compaction.auto)
|
||||||
|
if (compaction.threshold_percent != null) put("threshold_percent", compaction.threshold_percent)
|
||||||
|
if (compaction.prune != null) put("prune", compaction.prune)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
val skills = patch.skills
|
val skills = patch.skills
|
||||||
if (skills != null) {
|
if (skills != null) {
|
||||||
put("skills", buildJsonObject {
|
put("skills", buildJsonObject {
|
||||||
|
|||||||
+24
@@ -9,7 +9,9 @@ import ai.kilocode.backend.testing.FakeCliServer
|
|||||||
import ai.kilocode.backend.testing.MockCliServer
|
import ai.kilocode.backend.testing.MockCliServer
|
||||||
import ai.kilocode.backend.testing.TestLog
|
import ai.kilocode.backend.testing.TestLog
|
||||||
import ai.kilocode.rpc.dto.AgentConfigPatchDto
|
import ai.kilocode.rpc.dto.AgentConfigPatchDto
|
||||||
|
import ai.kilocode.rpc.dto.CompactionPatchDto
|
||||||
import ai.kilocode.rpc.dto.ConfigPatchDto
|
import ai.kilocode.rpc.dto.ConfigPatchDto
|
||||||
|
import ai.kilocode.rpc.dto.WatcherPatchDto
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -222,6 +224,28 @@ class KiloBackendAppServiceTest {
|
|||||||
assertEquals("fast", svc.config?.agent?.get("code")?.variant)
|
assertEquals("fast", svc.config?.agent?.get("code")?.variant)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `update config patches context settings and reloads`() = runBlocking {
|
||||||
|
val svc = create()
|
||||||
|
svc.connect()
|
||||||
|
ready(svc)
|
||||||
|
|
||||||
|
val state = svc.updateConfig(ConfigPatchDto(
|
||||||
|
watcher = WatcherPatchDto(ignore = listOf("**/dist/**", "tmp/**")),
|
||||||
|
compaction = CompactionPatchDto(auto = false, threshold_percent = 75.5, prune = false),
|
||||||
|
))
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"{\"watcher\":{\"ignore\":[\"**/dist/**\",\"tmp/**\"]},\"compaction\":{\"auto\":false,\"threshold_percent\":75.5,\"prune\":false}}",
|
||||||
|
mock.lastConfigPatchBody,
|
||||||
|
)
|
||||||
|
val cfg = appStateDto(state).config
|
||||||
|
assertEquals(listOf("**/dist/**", "tmp/**"), cfg?.watcher?.ignore)
|
||||||
|
assertEquals(false, cfg?.compaction?.auto)
|
||||||
|
assertEquals(75.5, cfg?.compaction?.threshold_percent)
|
||||||
|
assertEquals(false, svc.config?.compaction?.prune)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ready dto maps model config`() = runBlocking {
|
fun `ready dto maps model config`() = runBlocking {
|
||||||
mock.config = """{"model":"openai/gpt","agent":{"plan":{"model":"anthropic/claude","variant":"high"}}}"""
|
mock.config = """{"model":"openai/gpt","agent":{"plan":{"model":"anthropic/claude","variant":"high"}}}"""
|
||||||
|
|||||||
+40
@@ -4,6 +4,7 @@ import ai.kilocode.backend.workspace.CommandInfo
|
|||||||
import ai.kilocode.backend.workspace.ProviderData
|
import ai.kilocode.backend.workspace.ProviderData
|
||||||
import ai.kilocode.rpc.dto.ChatEventDto
|
import ai.kilocode.rpc.dto.ChatEventDto
|
||||||
import ai.kilocode.rpc.dto.AgentConfigPatchDto
|
import ai.kilocode.rpc.dto.AgentConfigPatchDto
|
||||||
|
import ai.kilocode.rpc.dto.CompactionPatchDto
|
||||||
import ai.kilocode.rpc.dto.ConfigDto
|
import ai.kilocode.rpc.dto.ConfigDto
|
||||||
import ai.kilocode.rpc.dto.ConfigPatchDto
|
import ai.kilocode.rpc.dto.ConfigPatchDto
|
||||||
import ai.kilocode.rpc.dto.ConfigUpdateDto
|
import ai.kilocode.rpc.dto.ConfigUpdateDto
|
||||||
@@ -19,6 +20,7 @@ import ai.kilocode.rpc.dto.PromptDto
|
|||||||
import ai.kilocode.rpc.dto.PromptPartDto
|
import ai.kilocode.rpc.dto.PromptPartDto
|
||||||
import ai.kilocode.rpc.dto.QuestionReplyDto
|
import ai.kilocode.rpc.dto.QuestionReplyDto
|
||||||
import ai.kilocode.rpc.dto.SkillsPatchDto
|
import ai.kilocode.rpc.dto.SkillsPatchDto
|
||||||
|
import ai.kilocode.rpc.dto.WatcherPatchDto
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
@@ -1173,6 +1175,21 @@ class KiloCliDataParserTest {
|
|||||||
assertEquals(listOf("https://example.test/skill.md"), cfg.skills?.urls)
|
assertEquals(listOf("https://example.test/skill.md"), cfg.skills?.urls)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parseConfig - context settings`() {
|
||||||
|
val cfg = KiloCliDataParser.parseConfig(
|
||||||
|
"""{
|
||||||
|
"watcher":{"ignore":["**/dist/**","tmp/**"]},
|
||||||
|
"compaction":{"auto":true,"threshold_percent":75.5,"prune":false}
|
||||||
|
}"""
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(listOf("**/dist/**", "tmp/**"), cfg.watcher?.ignore)
|
||||||
|
assertEquals(true, cfg.compaction?.auto)
|
||||||
|
assertEquals(75.5, cfg.compaction?.threshold_percent)
|
||||||
|
assertEquals(false, cfg.compaction?.prune)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `parseConfig - agent overrides and permissions`() {
|
fun `parseConfig - agent overrides and permissions`() {
|
||||||
val cfg = KiloCliDataParser.parseConfig(
|
val cfg = KiloCliDataParser.parseConfig(
|
||||||
@@ -2123,6 +2140,29 @@ class KiloCliDataParserTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `buildConfigPatch - context watcher and compaction fields`() {
|
||||||
|
val patch = ConfigPatchDto(
|
||||||
|
watcher = WatcherPatchDto(ignore = listOf("**/dist/**", "tmp/**")),
|
||||||
|
compaction = CompactionPatchDto(auto = false, threshold_percent = 75.5, prune = false),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"{\"watcher\":{\"ignore\":[\"**/dist/**\",\"tmp/**\"]},\"compaction\":{\"auto\":false,\"threshold_percent\":75.5,\"prune\":false}}",
|
||||||
|
KiloCliDataParser.buildConfigPatch(patch),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `buildConfigPatch - context threshold clear emits null`() {
|
||||||
|
val patch = ConfigPatchDto(compaction = CompactionPatchDto(clear = listOf("threshold_percent")))
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"{\"compaction\":{\"threshold_percent\":null}}",
|
||||||
|
KiloCliDataParser.buildConfigPatch(patch),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `buildConfigPatch - mcp upsert and delete`() {
|
fun `buildConfigPatch - mcp upsert and delete`() {
|
||||||
val patch = ConfigPatchDto(mcp = linkedMapOf(
|
val patch = ConfigPatchDto(mcp = linkedMapOf(
|
||||||
|
|||||||
+9
@@ -2,6 +2,7 @@ package ai.kilocode.client.settings
|
|||||||
|
|
||||||
import ai.kilocode.client.plugin.KiloBundle
|
import ai.kilocode.client.plugin.KiloBundle
|
||||||
import ai.kilocode.client.settings.agents.AgentBehaviorConfigurable
|
import ai.kilocode.client.settings.agents.AgentBehaviorConfigurable
|
||||||
|
import ai.kilocode.client.settings.context.ContextConfigurable
|
||||||
import ai.kilocode.client.settings.models.ModelsConfigurable
|
import ai.kilocode.client.settings.models.ModelsConfigurable
|
||||||
import ai.kilocode.client.settings.providers.ProvidersConfigurable
|
import ai.kilocode.client.settings.providers.ProvidersConfigurable
|
||||||
import ai.kilocode.client.settings.profile.UserProfileConfigurable
|
import ai.kilocode.client.settings.profile.UserProfileConfigurable
|
||||||
@@ -57,6 +58,14 @@ class KiloSettingsConfigurable : SearchableConfigurable {
|
|||||||
models.border = JBUI.Borders.emptyBottom(UiStyle.Gap.sm())
|
models.border = JBUI.Borders.emptyBottom(UiStyle.Gap.sm())
|
||||||
panel.next(models)
|
panel.next(models)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
val providers = ActionLink(KiloBundle.message("settings.providers.displayName")) { e ->
|
val providers = ActionLink(KiloBundle.message("settings.providers.displayName")) { e ->
|
||||||
val src = e.source as? JComponent ?: return@ActionLink
|
val src = e.source as? JComponent ?: return@ActionLink
|
||||||
val settings = Settings.KEY.getData(DataManager.getInstance().getDataContext(src)) ?: return@ActionLink
|
val settings = Settings.KEY.getData(DataManager.getInstance().getDataContext(src)) ?: return@ActionLink
|
||||||
|
|||||||
+18
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
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 ConfigPatchDto()
|
||||||
|
|
||||||
|
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 clear = mutableListOf<String>()
|
||||||
|
val threshold = parseThreshold(to.threshold)
|
||||||
|
val fromThreshold = parseThreshold(from.threshold)
|
||||||
|
if (fromThreshold != threshold && threshold == null) clear += "threshold_percent"
|
||||||
|
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()
|
||||||
|
}
|
||||||
+300
@@ -0,0 +1,300 @@
|
|||||||
|
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.SettingsStackedRow
|
||||||
|
import ai.kilocode.client.settings.base.SettingsToggle
|
||||||
|
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.ui.CollectionListModel
|
||||||
|
import com.intellij.ui.DocumentAdapter
|
||||||
|
import com.intellij.ui.ToolbarDecorator
|
||||||
|
import com.intellij.ui.components.JBList
|
||||||
|
import com.intellij.ui.components.JBTextField
|
||||||
|
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import javax.swing.JButton
|
||||||
|
import javax.swing.JComponent
|
||||||
|
import javax.swing.ListSelectionModel
|
||||||
|
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 { value -> update { copy(threshold = value) } }
|
||||||
|
private val patterns = PatternList { value -> update { copy(ignore = value) } }
|
||||||
|
|
||||||
|
init {
|
||||||
|
section(
|
||||||
|
KiloBundle.message("settings.context.compaction.title"),
|
||||||
|
KiloBundle.message("settings.context.compaction.description"),
|
||||||
|
).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"),
|
||||||
|
threshold.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(SettingsStackedRow(
|
||||||
|
KiloBundle.message("settings.context.watcher.patterns.title"),
|
||||||
|
KiloBundle.message("settings.context.watcher.patterns.description"),
|
||||||
|
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(
|
||||||
|
private val change: (String) -> Unit,
|
||||||
|
) : JBTextField() {
|
||||||
|
private var syncing = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
columns = THRESHOLD_COLUMNS
|
||||||
|
emptyText.text = KiloBundle.message("settings.context.compaction.threshold.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
|
||||||
|
return value.all { it.isDigit() || it == '.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class PatternList(
|
||||||
|
private val change: (List<String>) -> Unit,
|
||||||
|
) : Stack(StackAxis.VERTICAL, UiStyle.Gap.sm()) {
|
||||||
|
private val model = CollectionListModel<String>()
|
||||||
|
internal val entry = JBTextField().apply {
|
||||||
|
emptyText.text = KiloBundle.message("settings.context.watcher.placeholder")
|
||||||
|
}
|
||||||
|
private val add = JButton(KiloBundle.message("settings.context.watcher.add"), AllIcons.General.Add).apply {
|
||||||
|
addActionListener { add() }
|
||||||
|
}
|
||||||
|
private val list = JBList(model).apply {
|
||||||
|
selectionMode = ListSelectionModel.SINGLE_SELECTION
|
||||||
|
emptyText.text = KiloBundle.message("settings.context.watcher.empty")
|
||||||
|
}
|
||||||
|
private val panel = ToolbarDecorator.createDecorator(list)
|
||||||
|
.disableUpDownActions()
|
||||||
|
.disableAddAction()
|
||||||
|
.setRemoveAction { remove() }
|
||||||
|
.setRemoveActionUpdater { isEnabled && list.selectedIndex >= 0 }
|
||||||
|
.createPanel()
|
||||||
|
|
||||||
|
init {
|
||||||
|
entry.document.addDocumentListener(object : DocumentAdapter() {
|
||||||
|
override fun textChanged(e: DocumentEvent) = syncAdd()
|
||||||
|
})
|
||||||
|
entry.registerKeyboardAction(
|
||||||
|
{ add() },
|
||||||
|
javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_ENTER, 0),
|
||||||
|
JComponent.WHEN_FOCUSED,
|
||||||
|
)
|
||||||
|
next(Stack.horizontal(UiStyle.Gap.sm()).next(entry).next(add))
|
||||||
|
next(panel)
|
||||||
|
syncAdd()
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresEdt
|
||||||
|
fun sync(values: List<String>) {
|
||||||
|
if (model.items == values) return
|
||||||
|
model.replaceAll(values)
|
||||||
|
syncAdd()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setEnabled(enabled: Boolean) {
|
||||||
|
super.setEnabled(enabled)
|
||||||
|
entry.isEnabled = enabled
|
||||||
|
add.isEnabled = enabled && entry.text.trim().isNotBlank()
|
||||||
|
list.isEnabled = enabled
|
||||||
|
panel.isEnabled = enabled
|
||||||
|
syncAdd()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun add() {
|
||||||
|
val value = entry.text.trim()
|
||||||
|
if (!isEnabled || value.isBlank()) return
|
||||||
|
val values = model.items.toMutableList()
|
||||||
|
if (value !in values) values += value
|
||||||
|
entry.text = ""
|
||||||
|
model.replaceAll(values)
|
||||||
|
change(values)
|
||||||
|
syncAdd()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun remove() {
|
||||||
|
val idx = list.selectedIndex
|
||||||
|
if (!isEnabled || idx < 0 || idx >= model.size) return
|
||||||
|
val values = model.items.toMutableList()
|
||||||
|
values.removeAt(idx)
|
||||||
|
model.replaceAll(values)
|
||||||
|
change(values)
|
||||||
|
syncAdd()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun syncAdd() {
|
||||||
|
add.isEnabled = isEnabled && entry.text.trim().isNotBlank()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun summary(patch: ConfigPatchDto): String {
|
||||||
|
val parts = listOfNotNull(
|
||||||
|
"watcher".takeIf { patch.watcher != null },
|
||||||
|
"compaction".takeIf { patch.compaction != null },
|
||||||
|
)
|
||||||
|
return parts.joinToString(",").ifEmpty { "none" }
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val THRESHOLD_COLUMNS = 8
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<applicationConfigurable
|
<applicationConfigurable
|
||||||
parentId="ai.kilocode.jetbrains.settings"
|
parentId="ai.kilocode.jetbrains.settings"
|
||||||
id="ai.kilocode.jetbrains.settings.profile"
|
id="ai.kilocode.jetbrains.settings.profile"
|
||||||
groupWeight="4"
|
groupWeight="5"
|
||||||
instance="ai.kilocode.client.settings.profile.UserProfileConfigurable"
|
instance="ai.kilocode.client.settings.profile.UserProfileConfigurable"
|
||||||
bundle="messages.KiloBundle"
|
bundle="messages.KiloBundle"
|
||||||
key="settings.profile.displayName"/>
|
key="settings.profile.displayName"/>
|
||||||
@@ -41,11 +41,19 @@
|
|||||||
<applicationConfigurable
|
<applicationConfigurable
|
||||||
parentId="ai.kilocode.jetbrains.settings"
|
parentId="ai.kilocode.jetbrains.settings"
|
||||||
id="ai.kilocode.jetbrains.settings.models"
|
id="ai.kilocode.jetbrains.settings.models"
|
||||||
groupWeight="3"
|
groupWeight="4"
|
||||||
instance="ai.kilocode.client.settings.models.ModelsConfigurable"
|
instance="ai.kilocode.client.settings.models.ModelsConfigurable"
|
||||||
bundle="messages.KiloBundle"
|
bundle="messages.KiloBundle"
|
||||||
key="settings.models.displayName"/>
|
key="settings.models.displayName"/>
|
||||||
|
|
||||||
|
<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"/>
|
||||||
|
|
||||||
<applicationConfigurable
|
<applicationConfigurable
|
||||||
parentId="ai.kilocode.jetbrains.settings"
|
parentId="ai.kilocode.jetbrains.settings"
|
||||||
id="ai.kilocode.jetbrains.settings.providers"
|
id="ai.kilocode.jetbrains.settings.providers"
|
||||||
|
|||||||
@@ -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.title=Kilo Code is not connected
|
||||||
settings.cli.unavailable.message=Settings are available after Kilo Code connects to Core. If this does not resolve, restart the IDE or Kilo Core and try again.
|
settings.cli.unavailable.message=Settings are available after Kilo Code connects to Core. If this does not resolve, restart the IDE or Kilo Core and try again.
|
||||||
settings.models.displayName=Models
|
settings.models.displayName=Models
|
||||||
|
settings.context.displayName=Context
|
||||||
|
settings.context.description=Configure 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.placeholder=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.patterns.title=Ignored patterns
|
||||||
|
settings.context.watcher.patterns.description=Add one glob pattern per row.
|
||||||
|
settings.context.watcher.add=Add pattern
|
||||||
|
settings.context.watcher.empty=No ignore patterns configured.
|
||||||
|
settings.context.watcher.placeholder=e.g. **/dist/**
|
||||||
settings.providers.displayName=Providers
|
settings.providers.displayName=Providers
|
||||||
settings.agentBehavior.displayName=Agent Behavior
|
settings.agentBehavior.displayName=Agent Behavior
|
||||||
settings.agentBehavior.description=Configure agents, MCP servers, rules, workflows, and skills.
|
settings.agentBehavior.description=Configure agents, MCP servers, rules, workflows, and skills.
|
||||||
|
|||||||
+15
-1
@@ -1,6 +1,7 @@
|
|||||||
package ai.kilocode.client.settings
|
package ai.kilocode.client.settings
|
||||||
|
|
||||||
import ai.kilocode.client.settings.profile.UserProfileConfigurable
|
import ai.kilocode.client.settings.profile.UserProfileConfigurable
|
||||||
|
import ai.kilocode.client.settings.context.ContextConfigurable
|
||||||
import ai.kilocode.client.settings.models.ModelsConfigurable
|
import ai.kilocode.client.settings.models.ModelsConfigurable
|
||||||
import ai.kilocode.client.settings.agents.AgentBehaviorConfigurable
|
import ai.kilocode.client.settings.agents.AgentBehaviorConfigurable
|
||||||
import ai.kilocode.client.settings.providers.ProvidersConfigurable
|
import ai.kilocode.client.settings.providers.ProvidersConfigurable
|
||||||
@@ -29,6 +30,10 @@ class KiloSettingsConfigurableTest : BasePlatformTestCase() {
|
|||||||
assertEquals("ai.kilocode.jetbrains.settings.models", ModelsConfigurable.ID)
|
assertEquals("ai.kilocode.jetbrains.settings.models", ModelsConfigurable.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun `test child context id matches xml registration`() {
|
||||||
|
assertEquals("ai.kilocode.jetbrains.settings.context", ContextConfigurable.ID)
|
||||||
|
}
|
||||||
|
|
||||||
fun `test child provider and behavior ids match xml registration`() {
|
fun `test child provider and behavior ids match xml registration`() {
|
||||||
assertEquals("ai.kilocode.jetbrains.settings.providers", ProvidersConfigurable.ID)
|
assertEquals("ai.kilocode.jetbrains.settings.providers", ProvidersConfigurable.ID)
|
||||||
assertEquals("ai.kilocode.jetbrains.settings.agentBehavior", AgentBehaviorConfigurable.ID)
|
assertEquals("ai.kilocode.jetbrains.settings.agentBehavior", AgentBehaviorConfigurable.ID)
|
||||||
@@ -79,12 +84,21 @@ class KiloSettingsConfigurableTest : BasePlatformTestCase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun `test createComponent contains Context link`() {
|
||||||
|
val cfg = KiloSettingsConfigurable()
|
||||||
|
edt {
|
||||||
|
val panel = cfg.createComponent()
|
||||||
|
val links = links(panel as Container)
|
||||||
|
assertTrue("expected a link labeled 'Context'", links.any { it.text == "Context" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun `test createComponent contains settings links in order`() {
|
fun `test createComponent contains settings links in order`() {
|
||||||
val cfg = KiloSettingsConfigurable()
|
val cfg = KiloSettingsConfigurable()
|
||||||
edt {
|
edt {
|
||||||
val panel = cfg.createComponent()
|
val panel = cfg.createComponent()
|
||||||
val labels = links(panel as Container).map { it.text }
|
val labels = links(panel as Container).map { it.text }
|
||||||
assertEquals(listOf("User Profile", "Models", "Providers", "Agent Behavior"), labels)
|
assertEquals(listOf("User Profile", "Models", "Context", "Providers", "Agent Behavior"), labels)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+76
@@ -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/**"))
|
||||||
|
|
||||||
|
assertFalse(changed(patch(draft, draft)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@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`() {
|
||||||
|
val from = ContextDraft(threshold = "50")
|
||||||
|
val to = ContextDraft(threshold = "101")
|
||||||
|
|
||||||
|
assertEquals(ThresholdStatus.INVALID, thresholdStatus(to.threshold))
|
||||||
|
assertFalse(changed(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")))
|
||||||
|
}
|
||||||
|
}
|
||||||
+201
@@ -0,0 +1,201 @@
|
|||||||
|
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.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.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 javax.swing.AbstractButton
|
||||||
|
import javax.swing.JLabel
|
||||||
|
import javax.swing.JTextField
|
||||||
|
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 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 list = components(panel).filterIsInstance<PatternList>().single()
|
||||||
|
list.entry.text = "**/dist/**"
|
||||||
|
buttons(panel).single { it.text == "Add pattern" }.doClick()
|
||||||
|
panel.applyDraft()
|
||||||
|
}
|
||||||
|
|
||||||
|
flushUntil { rpc.configPatches.isNotEmpty() }
|
||||||
|
assertEquals(listOf("tmp/**", "**/dist/**"), rpc.configPatches.single().watcher?.ignore)
|
||||||
|
}
|
||||||
|
|
||||||
|
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): JTextField = components(panel)
|
||||||
|
.filterIsInstance<JTextField>()
|
||||||
|
.single { it.columns == 8 }
|
||||||
|
|
||||||
|
private fun buttons(panel: ContextSettingsUi): List<AbstractButton> = components(panel).filterIsInstance<AbstractButton>()
|
||||||
|
|
||||||
|
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) }
|
||||||
|
is JTextField -> 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -2,6 +2,7 @@ package ai.kilocode.client.testing
|
|||||||
|
|
||||||
import ai.kilocode.rpc.KiloAppRpcApi
|
import ai.kilocode.rpc.KiloAppRpcApi
|
||||||
import ai.kilocode.rpc.dto.AgentConfigDto
|
import ai.kilocode.rpc.dto.AgentConfigDto
|
||||||
|
import ai.kilocode.rpc.dto.CompactionConfigDto
|
||||||
import ai.kilocode.rpc.dto.ConfigDto
|
import ai.kilocode.rpc.dto.ConfigDto
|
||||||
import ai.kilocode.rpc.dto.ConfigPatchDto
|
import ai.kilocode.rpc.dto.ConfigPatchDto
|
||||||
import ai.kilocode.rpc.dto.DeviceAuthDto
|
import ai.kilocode.rpc.dto.DeviceAuthDto
|
||||||
@@ -16,6 +17,7 @@ import ai.kilocode.rpc.dto.ModelVariantUpdateDto
|
|||||||
import ai.kilocode.rpc.dto.ProfileDto
|
import ai.kilocode.rpc.dto.ProfileDto
|
||||||
import ai.kilocode.rpc.dto.SkillsConfigDto
|
import ai.kilocode.rpc.dto.SkillsConfigDto
|
||||||
import ai.kilocode.rpc.dto.TelemetryCaptureDto
|
import ai.kilocode.rpc.dto.TelemetryCaptureDto
|
||||||
|
import ai.kilocode.rpc.dto.WatcherConfigDto
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -196,12 +198,34 @@ class FakeAppRpcApi : KiloAppRpcApi {
|
|||||||
val mcp = patch.mcp?.entries?.fold(config.mcp) { acc, (name, item) ->
|
val mcp = patch.mcp?.entries?.fold(config.mcp) { acc, (name, item) ->
|
||||||
if (item == null) acc - name else acc + (name to item)
|
if (item == null) acc - name else acc + (name to item)
|
||||||
} ?: config.mcp
|
} ?: config.mcp
|
||||||
|
val watcher = patch.watcher?.let { item ->
|
||||||
|
val cfg = config.watcher
|
||||||
|
cfg?.copy(ignore = item.ignore ?: cfg.ignore)
|
||||||
|
?: WatcherConfigDto(ignore = item.ignore ?: emptyList())
|
||||||
|
} ?: config.watcher
|
||||||
|
val compaction = patch.compaction?.let { item ->
|
||||||
|
val cfg = item.clear.fold(config.compaction ?: CompactionConfigDto()) { next, field ->
|
||||||
|
when (field) {
|
||||||
|
"threshold_percent" -> next.copy(threshold_percent = null)
|
||||||
|
"auto" -> next.copy(auto = null)
|
||||||
|
"prune" -> next.copy(prune = null)
|
||||||
|
else -> next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg.copy(
|
||||||
|
auto = item.auto ?: cfg.auto,
|
||||||
|
threshold_percent = item.threshold_percent ?: cfg.threshold_percent,
|
||||||
|
prune = item.prune ?: cfg.prune,
|
||||||
|
)
|
||||||
|
} ?: config.compaction
|
||||||
return config.copy(
|
return config.copy(
|
||||||
defaultAgent = if (values.containsKey("default_agent")) values["default_agent"] else config.defaultAgent,
|
defaultAgent = if (values.containsKey("default_agent")) values["default_agent"] else config.defaultAgent,
|
||||||
model = if (values.containsKey("model")) values["model"] else config.model,
|
model = if (values.containsKey("model")) values["model"] else config.model,
|
||||||
smallModel = if (values.containsKey("small_model")) values["small_model"] else config.smallModel,
|
smallModel = if (values.containsKey("small_model")) values["small_model"] else config.smallModel,
|
||||||
subagentModel = if (values.containsKey("subagent_model")) values["subagent_model"] else config.subagentModel,
|
subagentModel = if (values.containsKey("subagent_model")) values["subagent_model"] else config.subagentModel,
|
||||||
subagentVariant = if (values.containsKey("subagent_variant")) values["subagent_variant"] else config.subagentVariant,
|
subagentVariant = if (values.containsKey("subagent_variant")) values["subagent_variant"] else config.subagentVariant,
|
||||||
|
watcher = watcher,
|
||||||
|
compaction = compaction,
|
||||||
instructions = patch.instructions ?: config.instructions,
|
instructions = patch.instructions ?: config.instructions,
|
||||||
skills = patch.skills?.let { SkillsConfigDto(paths = it.paths.orEmpty(), urls = it.urls.orEmpty()) } ?: config.skills,
|
skills = patch.skills?.let { SkillsConfigDto(paths = it.paths.orEmpty(), urls = it.urls.orEmpty()) } ?: config.skills,
|
||||||
mcp = mcp,
|
mcp = mcp,
|
||||||
|
|||||||
@@ -64,12 +64,26 @@ data class ConfigDto(
|
|||||||
val subagentModel: String? = null,
|
val subagentModel: String? = null,
|
||||||
val subagentVariant: String? = null,
|
val subagentVariant: String? = null,
|
||||||
val defaultAgent: String? = null,
|
val defaultAgent: String? = null,
|
||||||
|
val watcher: WatcherConfigDto? = null,
|
||||||
|
val compaction: CompactionConfigDto? = null,
|
||||||
val instructions: List<String> = emptyList(),
|
val instructions: List<String> = emptyList(),
|
||||||
val skills: SkillsConfigDto? = null,
|
val skills: SkillsConfigDto? = null,
|
||||||
val mcp: Map<String, McpConfigDto> = emptyMap(),
|
val mcp: Map<String, McpConfigDto> = emptyMap(),
|
||||||
val agent: Map<String, AgentConfigDto> = 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
|
@Serializable
|
||||||
data class SkillsConfigDto(
|
data class SkillsConfigDto(
|
||||||
val paths: List<String> = emptyList(),
|
val paths: List<String> = emptyList(),
|
||||||
@@ -109,12 +123,27 @@ sealed class PermissionRuleDto {
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class ConfigPatchDto(
|
data class ConfigPatchDto(
|
||||||
val values: Map<String, String?> = emptyMap(),
|
val values: Map<String, String?> = emptyMap(),
|
||||||
|
val watcher: WatcherPatchDto? = null,
|
||||||
|
val compaction: CompactionPatchDto? = null,
|
||||||
val instructions: List<String>? = null,
|
val instructions: List<String>? = null,
|
||||||
val skills: SkillsPatchDto? = null,
|
val skills: SkillsPatchDto? = null,
|
||||||
val mcp: Map<String, McpConfigDto?>? = null,
|
val mcp: Map<String, McpConfigDto?>? = null,
|
||||||
val agents: Map<String, AgentConfigPatchDto> = emptyMap(),
|
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
|
@Serializable
|
||||||
data class AgentConfigPatchDto(
|
data class AgentConfigPatchDto(
|
||||||
val clear: List<String> = emptyList(),
|
val clear: List<String> = emptyList(),
|
||||||
|
|||||||
Reference in New Issue
Block a user