mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-01 15:32:11 +08:00
Merge pull request #12292 from Kilo-Org/analyze-jetbrains-vscode-setting-parity
feat(jetbrains): add Context settings parity
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.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 {
|
||||
|
||||
+24
@@ -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"}}}"""
|
||||
|
||||
+57
@@ -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(
|
||||
|
||||
+9
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+12
-1
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+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"
|
||||
}
|
||||
}
|
||||
+77
@@ -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()
|
||||
}
|
||||
+388
@@ -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 l’enregistrement 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 qu’il n’atteigne 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 d’outils pendant la compaction
|
||||
settings.context.watcher.title=Motifs d’ignorance de l’observateur
|
||||
settings.context.watcher.description=Motifs glob pour les fichiers que l’observateur 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 d’ignorance configuré.
|
||||
settings.context.watcher.input.title=Ajouter un motif d’ignorance
|
||||
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:
|
||||
|
||||
+23
@@ -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-шаблон для ігнорування:
|
||||
|
||||
+23
@@ -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 模式:
|
||||
|
||||
+23
@@ -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 模式:
|
||||
|
||||
+15
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+60
@@ -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")
|
||||
|
||||
+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/**"))
|
||||
|
||||
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")))
|
||||
}
|
||||
}
|
||||
+324
@@ -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)
|
||||
}
|
||||
}
|
||||
+24
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user