mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
Merge remote-tracking branch 'origin/main' into marius-kilocode/kilo-opencode-v1.17.5
This commit is contained in:
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
"@kilocode/cli": minor
|
|
||||||
"kilo-code": minor
|
|
||||||
---
|
|
||||||
|
|
||||||
Allow agents to stop and remove a targeted Agent Manager session.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
"@kilocode/cli": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Improve CLI sidebar usage sections with cent-formatted costs, collapsible details, and aligned model totals.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
"kilo-code": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Respect the Terminal Command Blocks display setting for background process tool cards.
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
"@kilocode/cli": patch
|
|
||||||
"kilo-code": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Display here-document content as plain text in terminal approval prompts.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
"kilo-code": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Support bidirectional text in chat messages so right-to-left and mixed-language conversations render in the correct direction.
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
"@kilocode/cli": patch
|
|
||||||
"@kilocode/kilo-gateway": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Fix Cloud Agent session imports in installed CLI builds and prevent malformed exports or write failures from leaving partial imports.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
"kilo-code": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Open the Agent Manager new-worktree dialog from primary actions and keyboard shortcuts, with an explicit quick-create action in its dropdown.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
"kilo-code": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Keep sessions usable after deprecated local review commands complete without a session status transition.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
"@kilocode/cli": minor
|
|
||||||
"@kilocode/kilo-indexing": minor
|
|
||||||
"kilo-code": minor
|
|
||||||
---
|
|
||||||
|
|
||||||
Configure a custom file extension allowlist for codebase indexing to limit scans to relevant project files and support additional text formats.
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@kilocode/kilo-jetbrains": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Support viewing, opening, editing, deleting, and configuring JetBrains skill sources.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
"@kilocode/cli": patch
|
||||||
|
"@kilocode/kilo-memory": patch
|
||||||
|
"@kilocode/kilo-ui": patch
|
||||||
|
"kilo-code": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Simplify project memory settings and activity visibility, replace direct editing with folder inspection, add nested memory slash-command completion and status views, improve empty-project handling, compact native tool-call summaries, and remove legacy memory audit logs.
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
"@kilocode/kilo-indexing": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Allow remote embedder validation timeouts to retry before indexing reports a connection failure.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
"@kilocode/cli": minor
|
|
||||||
---
|
|
||||||
|
|
||||||
Remote CLI: expose slash command discovery, execution, and `/new` session creation over the relay. `list_commands` and `send_command` (including the built-in `compact` flow) are scoped to the current session's directory. `create_session` creates a root session in that directory, attaches it to the relay heartbeat set, and returns the new session id only after the heartbeat completes so the mobile client can navigate immediately. Failures are sanitized; the command is not auto-retried and the user may retry manually after a transient relay failure.
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"kilo-code": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix scroll position not resetting when switching between diff files in the chat and virtual diff viewer
|
||||||
@@ -160,7 +160,7 @@ jobs:
|
|||||||
if: matrix.settings.run
|
if: matrix.settings.run
|
||||||
run: bun turbo test:ci --output-logs=errors-only --log-order=grouped --log-prefix=task --filter='@kilocode/cli'
|
run: bun turbo test:ci --output-logs=errors-only --log-order=grouped --log-prefix=task --filter='@kilocode/cli'
|
||||||
env:
|
env:
|
||||||
KILO_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
|
KILO_EXPERIMENTAL_DISABLE_FILEWATCHER: "true" # kilocode_change - was Windows-only; the CLI now starts a watcher per instance, too heavy/racy for unit tests. Watcher tests opt back in.
|
||||||
KILO_TEST_PROFILE: ${{ matrix.settings.os == 'macos' && 'darwin' || '' }}
|
KILO_TEST_PROFILE: ${{ matrix.settings.os == 'macos' && 'darwin' || '' }}
|
||||||
KILO_TEST_SHARD: ${{ format('{0}/{1}', matrix.settings.index, matrix.settings.total) }}
|
KILO_TEST_SHARD: ${{ format('{0}/{1}', matrix.settings.index, matrix.settings.total) }}
|
||||||
# kilocode_change end
|
# kilocode_change end
|
||||||
|
|||||||
@@ -0,0 +1,423 @@
|
|||||||
|
# JetBrains Context Settings Page
|
||||||
|
|
||||||
|
Implement the Tier 1 Context settings from `docs/jetbrains-vscode-settings-parity.md` in the JetBrains plugin. This is a pure `kilo.json` settings UI: no CLI feature work, no SDK regen, and no session-rendering changes.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Add a new JetBrains settings page under `Settings -> Tools -> Kilo Code -> Context` for:
|
||||||
|
|
||||||
|
| Setting | Config key | Type |
|
||||||
|
|---|---|---|
|
||||||
|
| Auto-compaction | `compaction.auto` | boolean |
|
||||||
|
| Compaction threshold percent | `compaction.threshold_percent` | number or null |
|
||||||
|
| Prune on compaction | `compaction.prune` | boolean |
|
||||||
|
| Watcher ignore patterns | `watcher.ignore` | string array |
|
||||||
|
|
||||||
|
Do not include VS Code Context-tab memory/indexing controls in this first pass. JetBrains does not have the equivalent memory/indexing settings service yet, and the parity doc excludes indexing from easy wins.
|
||||||
|
|
||||||
|
Do not put `snapshot` on this page unless product explicitly decides to combine Context and Checkpoints. The parity doc suggests `snapshot` belongs on a new Checkpoints page.
|
||||||
|
|
||||||
|
## Context Verified
|
||||||
|
|
||||||
|
- Source parity doc: `docs/jetbrains-vscode-settings-parity.md`.
|
||||||
|
- JetBrains settings guidance: `packages/kilo-jetbrains/AGENTS.md`, especially `Settings UI`.
|
||||||
|
- Existing settings pages are registered in `packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml`.
|
||||||
|
- Existing page pattern to mirror:
|
||||||
|
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsConfigurable.kt`
|
||||||
|
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt`
|
||||||
|
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsState.kt`
|
||||||
|
- Existing global config write path is sufficient once DTO/parser support is added:
|
||||||
|
- Frontend: `KiloAppService.updateConfigAsync(...)`
|
||||||
|
- RPC: `KiloAppRpcApi.updateConfig(patch: ConfigPatchDto)`
|
||||||
|
- Backend: `KiloBackendAppService.updateConfig(...)`
|
||||||
|
- HTTP: `PATCH /global/config`, then `GET /global/config`
|
||||||
|
- Existing backend parser currently only serializes selected string keys from `ConfigPatchDto.values`; Context needs typed booleans, numbers, explicit null, and string arrays.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- Use global config for the first implementation, matching the existing app-level settings write path.
|
||||||
|
- Add typed DTO fields instead of overloading `ConfigPatchDto.values` for non-string values.
|
||||||
|
- Use an explicit `clear` list for nullable compaction fields, because `Double?` cannot distinguish absent from explicit `null`.
|
||||||
|
- Reuse `BaseSettingsUi`, `DraftReadyConfigurable`, `SettingsDraftState`, `SettingsRows`, `SettingsRow`, and `SettingsToggle`.
|
||||||
|
- Use the shared settings list primitives for `watcher.ignore`; do not build a bespoke add/remove list if `SettingsListPanel` or adjacent list primitives fit.
|
||||||
|
- Keep all UI strings in `KiloBundle.properties`. Let other locale bundles fall back unless the repo's resource-bundle checks require duplicated English keys.
|
||||||
|
|
||||||
|
## Part A - Shared DTOs
|
||||||
|
|
||||||
|
File: `packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/KiloAppStateDto.kt`
|
||||||
|
|
||||||
|
Add config read DTOs:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
@Serializable
|
||||||
|
data class WatcherConfigDto(
|
||||||
|
val ignore: List<String> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CompactionConfigDto(
|
||||||
|
val auto: Boolean? = null,
|
||||||
|
val threshold_percent: Double? = null,
|
||||||
|
val prune: Boolean? = null,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Extend `ConfigDto`:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val watcher: WatcherConfigDto? = null,
|
||||||
|
val compaction: CompactionConfigDto? = null,
|
||||||
|
```
|
||||||
|
|
||||||
|
Add patch DTOs:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
@Serializable
|
||||||
|
data class WatcherPatchDto(
|
||||||
|
val ignore: List<String>? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CompactionPatchDto(
|
||||||
|
val clear: List<String> = emptyList(),
|
||||||
|
val auto: Boolean? = null,
|
||||||
|
val threshold_percent: Double? = null,
|
||||||
|
val prune: Boolean? = null,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Extend `ConfigPatchDto`:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val watcher: WatcherPatchDto? = null,
|
||||||
|
val compaction: CompactionPatchDto? = null,
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- `watcher.ignore = null` means no change.
|
||||||
|
- `watcher.ignore = emptyList()` means explicitly save an empty list.
|
||||||
|
- `compaction.threshold_percent = null` alone means no change.
|
||||||
|
- `compaction.clear = listOf("threshold_percent")` means emit JSON `"threshold_percent": null`.
|
||||||
|
- `false` boolean values must be serialized; do not treat `false` as absent.
|
||||||
|
|
||||||
|
## Part B - Backend Config Parser And Serializer
|
||||||
|
|
||||||
|
File: `packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt`
|
||||||
|
|
||||||
|
### Parse
|
||||||
|
|
||||||
|
Extend `parseConfig(raw)` to read:
|
||||||
|
|
||||||
|
- `watcher.ignore`
|
||||||
|
- `compaction.auto`
|
||||||
|
- `compaction.threshold_percent`
|
||||||
|
- `compaction.prune`
|
||||||
|
|
||||||
|
Add private helpers near `parseSkillsConfig` / `parseMcpConfig`:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
private fun parseWatcherConfig(obj: JsonObject?): WatcherConfigDto?
|
||||||
|
private fun parseCompactionConfig(obj: JsonObject?): CompactionConfigDto?
|
||||||
|
```
|
||||||
|
|
||||||
|
Use existing helper style:
|
||||||
|
|
||||||
|
- strings: `str(...)`
|
||||||
|
- booleans: `flagOrNull(...)`
|
||||||
|
- numbers: `num(...)`
|
||||||
|
- arrays: `arr()?.mapNotNull { it.jsonPrimitive.contentOrNull }`
|
||||||
|
|
||||||
|
### Serialize
|
||||||
|
|
||||||
|
Extend `buildConfigPatch(patch)` to emit typed context patches:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"watcher": {
|
||||||
|
"ignore": ["**/node_modules/**"]
|
||||||
|
},
|
||||||
|
"compaction": {
|
||||||
|
"auto": true,
|
||||||
|
"threshold_percent": 80,
|
||||||
|
"prune": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For explicit threshold clearing:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compaction": {
|
||||||
|
"threshold_percent": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep the existing `values` allowlist for string model keys. Do not pass Context values through `values`.
|
||||||
|
|
||||||
|
## Part C - Frontend State Model
|
||||||
|
|
||||||
|
Add package: `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/context/`
|
||||||
|
|
||||||
|
New file: `ContextSettingsState.kt`
|
||||||
|
|
||||||
|
Define:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
internal data class ContextDraft(
|
||||||
|
val auto: Boolean? = null,
|
||||||
|
val threshold: String = "",
|
||||||
|
val prune: Boolean? = null,
|
||||||
|
val ignore: List<String> = emptyList(),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Use a string for the threshold draft so the UI can represent blank/invalid intermediate input without losing user text. Convert only when building a patch.
|
||||||
|
|
||||||
|
Functions to add:
|
||||||
|
|
||||||
|
- `contextDraft(config: ConfigDto?): ContextDraft`
|
||||||
|
- `patch(from: ContextDraft, to: ContextDraft): ConfigPatchDto`
|
||||||
|
- `savedMatches(base: ContextDraft, draft: ContextDraft): Boolean`
|
||||||
|
- `threshold(value: String): Double?` or equivalent parsing helper
|
||||||
|
- validation helper for threshold range if desired
|
||||||
|
|
||||||
|
Patch behavior:
|
||||||
|
|
||||||
|
- Only emit changed fields.
|
||||||
|
- Emit `CompactionPatchDto(auto = false)` when the user turns auto-compaction off.
|
||||||
|
- Emit `CompactionPatchDto(prune = false)` when the user turns pruning off.
|
||||||
|
- Emit `CompactionPatchDto(threshold_percent = 80.0)` for a non-blank valid number.
|
||||||
|
- Emit `CompactionPatchDto(clear = listOf("threshold_percent"))` when an existing threshold is cleared.
|
||||||
|
- Emit `WatcherPatchDto(ignore = emptyList())` when the last ignore pattern is removed.
|
||||||
|
- Return no change from the page when all fields match the baseline.
|
||||||
|
|
||||||
|
## Part D - Frontend UI Page
|
||||||
|
|
||||||
|
New file: `ContextConfigurable.kt`
|
||||||
|
|
||||||
|
Mirror `ModelsConfigurable`:
|
||||||
|
|
||||||
|
- Extend `DraftReadyConfigurable<JComponent>`.
|
||||||
|
- `ID = "ai.kilocode.jetbrains.settings.context"`.
|
||||||
|
- `getDisplayName()` returns `KiloBundle.message("settings.context.displayName")`.
|
||||||
|
- `create(cs)` returns `ContextSettingsUi(cs)`.
|
||||||
|
|
||||||
|
New file: `ContextSettingsUi.kt`
|
||||||
|
|
||||||
|
Mirror the simple parts of `ModelsSettingsUi`:
|
||||||
|
|
||||||
|
- Extend `BaseSettingsUi<ContextSettingsContent, ContextDraft, ConfigPatchDto, KiloAppStateDto, Unit>`.
|
||||||
|
- Initial draft is `ContextDraft()`.
|
||||||
|
- `save(change, done)` calls `app.updateConfigAsync(change, done)`.
|
||||||
|
- `base(result)` and `draft(state)` call `contextDraft(state.config)`.
|
||||||
|
- `saved(base, draft)` calls `savedMatches(base, draft)`.
|
||||||
|
- `pendingText()` uses `settings.context.save.pending`.
|
||||||
|
- `failedText()` uses `settings.context.save.failed`.
|
||||||
|
- `loadWorkspace(root)` returns `Unit`; `applyWorkspace(result)` is `Unit`.
|
||||||
|
- `models(state)` is `Unit`.
|
||||||
|
- `syncContent()` updates enabled states, field values, save/progress overlay, and validation messaging.
|
||||||
|
|
||||||
|
New content class: `ContextSettingsContent`
|
||||||
|
|
||||||
|
Suggested layout:
|
||||||
|
|
||||||
|
- Section `settings.context.compaction.title`
|
||||||
|
- Toggle row `settings.context.compaction.auto.title`
|
||||||
|
- Numeric row `settings.context.compaction.threshold.title`
|
||||||
|
- Toggle row `settings.context.compaction.prune.title`
|
||||||
|
- Section `settings.context.watcher.title`
|
||||||
|
- List editor row/panel for ignore patterns
|
||||||
|
|
||||||
|
Controls:
|
||||||
|
|
||||||
|
- Use `SettingsToggle` for booleans.
|
||||||
|
- Use `JBTextField` or a small reusable numeric field pattern based on `AgentEditDialog` for threshold.
|
||||||
|
- Use shared list primitives (`SettingsListPanel` / `SettingsListView` / `SettingsListItem` / `SettingsListCell`) for `watcher.ignore` where practical.
|
||||||
|
- Keep the page editable while app status is ready and no save is pending.
|
||||||
|
- Disable controls while saving.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- Blank threshold is valid and means clear/reset the config value if it differs from baseline.
|
||||||
|
- Non-numeric threshold is invalid and should prevent `apply()` from sending a patch.
|
||||||
|
- Suggested accepted range is `0..100`; if existing CLI allows a broader range, follow CLI behavior.
|
||||||
|
- Show validation through existing settings messaging rather than custom ad hoc labels.
|
||||||
|
|
||||||
|
## Part E - Settings Registration And Root Navigation
|
||||||
|
|
||||||
|
File: `packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml`
|
||||||
|
|
||||||
|
Add a child configurable:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<applicationConfigurable
|
||||||
|
parentId="ai.kilocode.jetbrains.settings"
|
||||||
|
id="ai.kilocode.jetbrains.settings.context"
|
||||||
|
groupWeight="3"
|
||||||
|
instance="ai.kilocode.client.settings.context.ContextConfigurable"
|
||||||
|
bundle="messages.KiloBundle"
|
||||||
|
key="settings.context.displayName"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Adjust weights so the desired order is stable. Recommended order:
|
||||||
|
|
||||||
|
| Page | Weight |
|
||||||
|
|---|---|
|
||||||
|
| User Profile | 5 |
|
||||||
|
| Models | 4 |
|
||||||
|
| Context | 3 |
|
||||||
|
| Providers | 2 |
|
||||||
|
| Agent Behavior | 1 |
|
||||||
|
|
||||||
|
File: `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/KiloSettingsConfigurable.kt`
|
||||||
|
|
||||||
|
Add a root-page `ActionLink` for Context between Models and Providers:
|
||||||
|
|
||||||
|
- Import `ContextConfigurable`.
|
||||||
|
- Link text: `settings.context.displayName`.
|
||||||
|
- Link target: `ContextConfigurable.ID`.
|
||||||
|
|
||||||
|
`KiloSettingsSelection.kt` probably needs no code changes because child IDs already share the root prefix.
|
||||||
|
|
||||||
|
## Part F - Strings
|
||||||
|
|
||||||
|
File: `packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties`
|
||||||
|
|
||||||
|
Add base strings near the other settings strings:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
settings.context.displayName=Context
|
||||||
|
settings.context.description=Configure compaction and file-watcher context behavior.
|
||||||
|
settings.context.save.pending=Saving context settings...
|
||||||
|
settings.context.save.failed=Failed to save context settings
|
||||||
|
settings.context.compaction.title=Compaction
|
||||||
|
settings.context.compaction.description=Control when Kilo summarizes long sessions to reduce context usage.
|
||||||
|
settings.context.compaction.auto.title=Auto-compaction
|
||||||
|
settings.context.compaction.auto.description=Automatically compact long conversations before they exceed the model context window.
|
||||||
|
settings.context.compaction.threshold.title=Compaction threshold
|
||||||
|
settings.context.compaction.threshold.description=Percent of the context window to use before auto-compaction starts. Leave blank to use the default.
|
||||||
|
settings.context.compaction.threshold.invalid=Enter a number from 0 to 100, or leave the field blank.
|
||||||
|
settings.context.compaction.prune.title=Prune on compaction
|
||||||
|
settings.context.compaction.prune.description=Drop older raw conversation details after compaction to keep the session context smaller.
|
||||||
|
settings.context.watcher.title=Watcher ignore patterns
|
||||||
|
settings.context.watcher.description=Glob patterns Kilo should ignore when watching repository file changes.
|
||||||
|
settings.context.watcher.add=Add pattern
|
||||||
|
settings.context.watcher.empty=No ignore patterns configured.
|
||||||
|
settings.context.watcher.placeholder=e.g. **/dist/**
|
||||||
|
settings.context.watcher.remove=Remove {0}
|
||||||
|
```
|
||||||
|
|
||||||
|
If resource-bundle tests require every key in every locale bundle, copy English values into the localized bundles and leave translation work for a later i18n pass.
|
||||||
|
|
||||||
|
## Part G - Test Updates
|
||||||
|
|
||||||
|
### Frontend state tests
|
||||||
|
|
||||||
|
Add: `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/context/ContextSettingsStateTest.kt`
|
||||||
|
|
||||||
|
Cover:
|
||||||
|
|
||||||
|
- Draft reads `ConfigDto.watcher` and `ConfigDto.compaction`.
|
||||||
|
- Unchanged draft emits no patch.
|
||||||
|
- Boolean changes emit `false` and `true` correctly.
|
||||||
|
- Threshold set emits `threshold_percent`.
|
||||||
|
- Threshold clear emits `clear = listOf("threshold_percent")`.
|
||||||
|
- Watcher list add/remove emits the whole new `ignore` list, including empty list.
|
||||||
|
- Invalid threshold is rejected before save if validation lives in state helpers.
|
||||||
|
|
||||||
|
### Frontend UI tests
|
||||||
|
|
||||||
|
Add: `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/context/ContextSettingsUiTest.kt`
|
||||||
|
|
||||||
|
Use `ModelsSettingsUiTest` as the main pattern:
|
||||||
|
|
||||||
|
- `BasePlatformTestCase`.
|
||||||
|
- Real EDT.
|
||||||
|
- `FakeAppRpcApi`.
|
||||||
|
- `KiloAppService`.
|
||||||
|
- `flushUntil` helpers.
|
||||||
|
- Assert `rpc.configPatches` after user interaction.
|
||||||
|
- Assert controls disable during pending save.
|
||||||
|
- Assert failed save leaves page modified and shows `settings.context.save.failed`.
|
||||||
|
|
||||||
|
Update `FakeAppRpcApi`:
|
||||||
|
|
||||||
|
- File: `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAppRpcApi.kt`
|
||||||
|
- Apply `patch.watcher` and `patch.compaction` to fake config state.
|
||||||
|
- Preserve explicit empty lists.
|
||||||
|
- Preserve boolean `false`.
|
||||||
|
- Honor `compaction.clear` by setting cleared fields to `null`.
|
||||||
|
|
||||||
|
### Backend parser tests
|
||||||
|
|
||||||
|
Update: `packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt`
|
||||||
|
|
||||||
|
Add exact JSON tests for:
|
||||||
|
|
||||||
|
- `parseConfig` reads watcher and compaction fields.
|
||||||
|
- `buildConfigPatch` emits watcher ignore arrays.
|
||||||
|
- `buildConfigPatch` emits `auto=false` and `prune=false`.
|
||||||
|
- `buildConfigPatch` emits numeric `threshold_percent`.
|
||||||
|
- `buildConfigPatch` emits explicit `threshold_percent:null` when `clear` includes the field.
|
||||||
|
|
||||||
|
### Backend app service tests
|
||||||
|
|
||||||
|
Update: `packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendAppServiceTest.kt`
|
||||||
|
|
||||||
|
Add a test similar to the existing model config update test:
|
||||||
|
|
||||||
|
- Call `updateConfig(ConfigPatchDto(watcher = ..., compaction = ...))`.
|
||||||
|
- Assert `MockCliServer.lastConfigPatchBody` exactly matches the expected nested JSON.
|
||||||
|
- Assert the returned/reloaded `ConfigDto` includes the saved Context values.
|
||||||
|
|
||||||
|
### Root settings tests
|
||||||
|
|
||||||
|
Update: `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/KiloSettingsConfigurableTest.kt`
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
- `ContextConfigurable.ID == "ai.kilocode.jetbrains.settings.context"`.
|
||||||
|
- Root page includes a Context link.
|
||||||
|
- Link order matches XML order.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Run from `packages/kilo-jetbrains/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew typecheck
|
||||||
|
./gradlew test
|
||||||
|
```
|
||||||
|
|
||||||
|
Focused checks while iterating:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :shared:test --tests '*ContextSettingsStateTest'
|
||||||
|
./gradlew :frontend:test --tests '*ContextSettingsUiTest'
|
||||||
|
./gradlew :backend:test --tests '*KiloCliDataParserTest'
|
||||||
|
./gradlew :backend:test --tests '*KiloBackendAppServiceTest'
|
||||||
|
```
|
||||||
|
|
||||||
|
If the exact Gradle module test selectors differ, run the package-level `./gradlew test` before marking the implementation ready.
|
||||||
|
|
||||||
|
Manual verification:
|
||||||
|
|
||||||
|
1. Run `./gradlew runIde` from `packages/kilo-jetbrains/`.
|
||||||
|
2. Open `Settings -> Tools -> Kilo Code -> Context`.
|
||||||
|
3. Toggle auto-compaction and prune.
|
||||||
|
4. Set threshold to a number, apply, reopen settings, and verify it persists.
|
||||||
|
5. Clear threshold, apply, reopen settings, and verify it resets.
|
||||||
|
6. Add and remove watcher ignore patterns, apply, reopen settings, and verify the list persists.
|
||||||
|
7. Inspect the global Kilo config file through the existing `Open: global ...` action if needed.
|
||||||
|
|
||||||
|
## Risks And Follow-ups
|
||||||
|
|
||||||
|
- Global vs project-local config: this plan uses the existing global config write path. Project-local Context settings would need new workspace config RPC plumbing.
|
||||||
|
- Threshold null semantics: implement explicit clear handling; otherwise clearing the field will silently do nothing.
|
||||||
|
- String-array UI: reuse list primitives even if it takes a small adapter type; avoid one-off list widgets.
|
||||||
|
- VS Code memory/indexing parity: defer because it is not pure config and is excluded by the easy-win criteria.
|
||||||
|
- Checkpoints page: implement `snapshot` separately unless product asks to combine it with Context.
|
||||||
|
- Changeset: when implementing this user-facing JetBrains settings feature, add a patch changeset for `kilo-code`/JetBrains according to repo release guidance.
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -127,7 +127,7 @@
|
|||||||
},
|
},
|
||||||
"packages/effect-drizzle-sqlite": {
|
"packages/effect-drizzle-sqlite": {
|
||||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
},
|
},
|
||||||
"packages/effect-sqlite-node": {
|
"packages/effect-sqlite-node": {
|
||||||
"name": "@opencode-ai/effect-sqlite-node",
|
"name": "@opencode-ai/effect-sqlite-node",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
},
|
},
|
||||||
"packages/http-recorder": {
|
"packages/http-recorder": {
|
||||||
"name": "@opencode-ai/http-recorder",
|
"name": "@opencode-ai/http-recorder",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "4.0.0-beta.74",
|
"@effect/platform-node": "4.0.0-beta.74",
|
||||||
"@effect/platform-node-shared": "4.0.0-beta.74",
|
"@effect/platform-node-shared": "4.0.0-beta.74",
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
},
|
},
|
||||||
"packages/kilo-console": {
|
"packages/kilo-console": {
|
||||||
"name": "@kilocode/kilo-console",
|
"name": "@kilocode/kilo-console",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kilocode/kilo-indexing": "workspace:*",
|
"@kilocode/kilo-indexing": "workspace:*",
|
||||||
"@kilocode/kilo-web-ui": "workspace:*",
|
"@kilocode/kilo-web-ui": "workspace:*",
|
||||||
@@ -197,7 +197,7 @@
|
|||||||
},
|
},
|
||||||
"packages/kilo-docs": {
|
"packages/kilo-docs": {
|
||||||
"name": "@kilocode/kilo-docs",
|
"name": "@kilocode/kilo-docs",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docsearch/css": "^4",
|
"@docsearch/css": "^4",
|
||||||
"@docsearch/js": "^4",
|
"@docsearch/js": "^4",
|
||||||
@@ -227,7 +227,7 @@
|
|||||||
},
|
},
|
||||||
"packages/kilo-gateway": {
|
"packages/kilo-gateway": {
|
||||||
"name": "@kilocode/kilo-gateway",
|
"name": "@kilocode/kilo-gateway",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/alibaba": "1.0.17",
|
"@ai-sdk/alibaba": "1.0.17",
|
||||||
"@ai-sdk/anthropic": "3.0.71",
|
"@ai-sdk/anthropic": "3.0.71",
|
||||||
@@ -263,7 +263,7 @@
|
|||||||
},
|
},
|
||||||
"packages/kilo-i18n": {
|
"packages/kilo-i18n": {
|
||||||
"name": "@kilocode/kilo-i18n",
|
"name": "@kilocode/kilo-i18n",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
@@ -273,7 +273,7 @@
|
|||||||
},
|
},
|
||||||
"packages/kilo-indexing": {
|
"packages/kilo-indexing": {
|
||||||
"name": "@kilocode/kilo-indexing",
|
"name": "@kilocode/kilo-indexing",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-bedrock-runtime": "3.1005.0",
|
"@aws-sdk/client-bedrock-runtime": "3.1005.0",
|
||||||
"@aws-sdk/credential-provider-ini": "3.972.31",
|
"@aws-sdk/credential-provider-ini": "3.972.31",
|
||||||
@@ -309,7 +309,7 @@
|
|||||||
},
|
},
|
||||||
"packages/kilo-memory": {
|
"packages/kilo-memory": {
|
||||||
"name": "@kilocode/kilo-memory",
|
"name": "@kilocode/kilo-memory",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
@@ -323,7 +323,7 @@
|
|||||||
},
|
},
|
||||||
"packages/kilo-sandbox": {
|
"packages/kilo-sandbox": {
|
||||||
"name": "@kilocode/sandbox",
|
"name": "@kilocode/sandbox",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sandbox-runtime": "catalog:",
|
"@anthropic-ai/sandbox-runtime": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -338,7 +338,7 @@
|
|||||||
},
|
},
|
||||||
"packages/kilo-telemetry": {
|
"packages/kilo-telemetry": {
|
||||||
"name": "@kilocode/kilo-telemetry",
|
"name": "@kilocode/kilo-telemetry",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kilocode/kilo-gateway": "workspace:*",
|
"@kilocode/kilo-gateway": "workspace:*",
|
||||||
"posthog-node": "4.4.0",
|
"posthog-node": "4.4.0",
|
||||||
@@ -352,7 +352,7 @@
|
|||||||
},
|
},
|
||||||
"packages/kilo-ui": {
|
"packages/kilo-ui": {
|
||||||
"name": "@kilocode/kilo-ui",
|
"name": "@kilocode/kilo-ui",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kilocode/sdk": "workspace:*",
|
"@kilocode/sdk": "workspace:*",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
@@ -389,7 +389,7 @@
|
|||||||
},
|
},
|
||||||
"packages/kilo-vscode": {
|
"packages/kilo-vscode": {
|
||||||
"name": "kilo-code",
|
"name": "kilo-code",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.39.0",
|
"@anthropic-ai/sdk": "^0.39.0",
|
||||||
"@kilocode/kilo-gateway": "workspace:*",
|
"@kilocode/kilo-gateway": "workspace:*",
|
||||||
@@ -458,7 +458,7 @@
|
|||||||
},
|
},
|
||||||
"packages/kilo-web-ui": {
|
"packages/kilo-web-ui": {
|
||||||
"name": "@kilocode/kilo-web-ui",
|
"name": "@kilocode/kilo-web-ui",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kilocode/kilo-ui": "workspace:*",
|
"@kilocode/kilo-ui": "workspace:*",
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
@@ -475,7 +475,7 @@
|
|||||||
},
|
},
|
||||||
"packages/llm": {
|
"packages/llm": {
|
||||||
"name": "@opencode-ai/llm",
|
"name": "@opencode-ai/llm",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@smithy/eventstream-codec": "4.2.14",
|
"@smithy/eventstream-codec": "4.2.14",
|
||||||
"@smithy/util-utf8": "4.2.2",
|
"@smithy/util-utf8": "4.2.2",
|
||||||
@@ -493,7 +493,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "@kilocode/cli",
|
"name": "@kilocode/cli",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"bin": {
|
"bin": {
|
||||||
"kilo": "./bin/kilo",
|
"kilo": "./bin/kilo",
|
||||||
"kilocode": "./bin/kilo",
|
"kilocode": "./bin/kilo",
|
||||||
@@ -660,7 +660,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@kilocode/plugin",
|
"name": "@kilocode/plugin",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kilocode/sdk": "workspace:*",
|
"@kilocode/sdk": "workspace:*",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -688,7 +688,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin-atomic-chat": {
|
"packages/plugin-atomic-chat": {
|
||||||
"name": "@kilocode/plugin-atomic-chat",
|
"name": "@kilocode/plugin-atomic-chat",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kilocode/plugin": "workspace:*",
|
"@kilocode/plugin": "workspace:*",
|
||||||
},
|
},
|
||||||
@@ -702,7 +702,7 @@
|
|||||||
},
|
},
|
||||||
"packages/script": {
|
"packages/script": {
|
||||||
"name": "@opencode-ai/script",
|
"name": "@opencode-ai/script",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
},
|
},
|
||||||
@@ -713,7 +713,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@kilocode/sdk",
|
"name": "@kilocode/sdk",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "catalog:",
|
"cross-spawn": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -728,7 +728,7 @@
|
|||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@opencode-ai/server",
|
"name": "@opencode-ai/server",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
@@ -742,7 +742,7 @@
|
|||||||
},
|
},
|
||||||
"packages/storybook": {
|
"packages/storybook": {
|
||||||
"name": "@opencode-ai/storybook",
|
"name": "@opencode-ai/storybook",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
@@ -765,7 +765,7 @@
|
|||||||
},
|
},
|
||||||
"packages/tui": {
|
"packages/tui": {
|
||||||
"name": "@opencode-ai/tui",
|
"name": "@opencode-ai/tui",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kilocode/plugin": "workspace:*",
|
"@kilocode/plugin": "workspace:*",
|
||||||
"@kilocode/sdk": "workspace:*",
|
"@kilocode/sdk": "workspace:*",
|
||||||
@@ -792,7 +792,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kilocode/sdk": "workspace:*",
|
"@kilocode/sdk": "workspace:*",
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
@@ -844,23 +844,21 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"trustedDependencies": [
|
"trustedDependencies": [
|
||||||
"tree-sitter-powershell",
|
|
||||||
"web-tree-sitter",
|
|
||||||
"tree-sitter-bash",
|
|
||||||
"esbuild",
|
"esbuild",
|
||||||
"protobufjs",
|
"protobufjs",
|
||||||
|
"web-tree-sitter",
|
||||||
|
"tree-sitter-bash",
|
||||||
],
|
],
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
|
||||||
"virtua@0.49.1": "patches/virtua@0.49.1.patch",
|
|
||||||
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch",
|
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch",
|
||||||
"@ai-sdk/xai@3.0.92": "patches/@ai-sdk%2Fxai@3.0.92.patch",
|
"@ai-sdk/xai@3.0.92": "patches/@ai-sdk%2Fxai@3.0.92.patch",
|
||||||
"@ff-labs/fff-bun@0.9.4": "patches/@ff-labs%2Ffff-bun@0.9.4.patch",
|
"virtua@0.49.1": "patches/virtua@0.49.1.patch",
|
||||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
|
||||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
|
||||||
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
|
||||||
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
||||||
|
"@ff-labs/fff-bun@0.9.4": "patches/@ff-labs%2Ffff-bun@0.9.4.patch",
|
||||||
"pacote@21.5.1": "patches/pacote@21.5.1.patch",
|
"pacote@21.5.1": "patches/pacote@21.5.1.patch",
|
||||||
|
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||||
|
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
||||||
|
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@effect/platform-node-shared": "4.0.0-beta.74",
|
"@effect/platform-node-shared": "4.0.0-beta.74",
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# JetBrains ↔ VS Code Settings Parity: Easy Wins
|
||||||
|
|
||||||
|
## How parity works here
|
||||||
|
|
||||||
|
Both clients edit the **same shared `kilo.json`** through the CLI. So any setting whose
|
||||||
|
behavior lives entirely in the CLI is an "easy win" for JetBrains: the CLI already does the
|
||||||
|
work, JetBrains just needs a UI row that writes the config key. No CLI changes, no new feature.
|
||||||
|
|
||||||
|
Structural gap: today JetBrains only has **Models / Providers / Agent Behavior / Profile**
|
||||||
|
settings pages. There is **no General / Display / Experimental / Context / Checkpoints** page.
|
||||||
|
The lift for most easy wins is:
|
||||||
|
|
||||||
|
1. Add a new `Configurable` page (using existing `settings/base/` primitives —
|
||||||
|
`BaseSettingsUi`, `SettingsRow`, `SettingsToggle`, `SettingsListPanel`), register it in
|
||||||
|
`kilo.jetbrains.frontend.xml`.
|
||||||
|
2. Extend the `buildConfigPatch` allowlist in `KiloCliDataParser.kt` (currently only
|
||||||
|
`model`, `small_model`, `subagent_model`, `subagent_variant`, `default_agent`) and add
|
||||||
|
boolean/number JSON serialization — it currently only emits strings.
|
||||||
|
3. Add localized labels to `KiloBundle.properties`.
|
||||||
|
|
||||||
|
No CLI/SDK change and no new runtime feature.
|
||||||
|
|
||||||
|
## Excluded from "easy"
|
||||||
|
|
||||||
|
| Excluded | Reason |
|
||||||
|
|---|---|
|
||||||
|
| Agent Behavior, Auto-Approve | Skipped by request |
|
||||||
|
| Indexing, Sandboxing | Imply enabling new features |
|
||||||
|
| Browser Automation | Playwright feature not present in JetBrains |
|
||||||
|
| Autocomplete (provider/model/toggles) | No autocomplete feature (flags exist only as migration stubs) |
|
||||||
|
| Agent Manager (auto-branch, prefix) | VS Code-only feature |
|
||||||
|
| Notification/attention sounds | Client must implement sound playback |
|
||||||
|
| `maxCost` alert | Client must render the alert UI |
|
||||||
|
| Commit message (`commit_message.prompt`, `languageCommitMessage`) | No commit-message generation feature in JetBrains |
|
||||||
|
| `language`, `fontSize`, `diff.renderMarkdown`, `agentWorkStyle` | VS Code-webview/onboarding-specific |
|
||||||
|
|
||||||
|
## Tier 1 — Genuinely easy (CLI does all the work; just add UI + config key)
|
||||||
|
|
||||||
|
| Setting | Config key | Type | Suggested page |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Hide prompt-training models | `hide_prompt_training_models` | bool | Models |
|
||||||
|
| Enable checkpoints | `snapshot` | bool | new "Checkpoints" |
|
||||||
|
| Auto-compaction | `compaction.auto` | bool | new "Context" |
|
||||||
|
| Compaction threshold % | `compaction.threshold_percent` | number | Context |
|
||||||
|
| Prune on compaction | `compaction.prune` | bool | Context |
|
||||||
|
| Watcher ignore patterns | `watcher.ignore` | string[] | Context (list editor) |
|
||||||
|
| Display username | `username` | string | new "Display/General" |
|
||||||
|
| Share mode | `share` | enum (manual/auto/disabled) | new "Experimental" |
|
||||||
|
| Remote control on startup | `remote_control` | bool | Experimental |
|
||||||
|
| Formatter integration | `formatter` | bool | Experimental |
|
||||||
|
| LSP integration | `lsp` | bool | Experimental |
|
||||||
|
| Batch tool | `experimental.batch_tool` | bool | Experimental |
|
||||||
|
| Native notebook tools | `experimental.native_notebook_tools` | bool | Experimental |
|
||||||
|
| Continue loop on deny | `experimental.continue_loop_on_deny` | bool | Experimental |
|
||||||
|
| SWE pruner (+ model) | `experimental.swe_pruner`, `..._model` | bool + string | Experimental |
|
||||||
|
| MCP timeout | `experimental.mcp_timeout` | number | Experimental |
|
||||||
|
| Per-tool toggles | `tools.<name>` | bool | Experimental |
|
||||||
|
|
||||||
|
**Claude Code compatibility**: lives under "Agent Behavior" in VS Code, but in JetBrains the
|
||||||
|
entire backend (`KiloClaudeCompatSettings` + RPC getter/setter + spawn-env wiring) already
|
||||||
|
exists with no UI. Exposing it is the single lowest-effort item — just a checkbox bound to the
|
||||||
|
existing RPC, no config plumbing.
|
||||||
|
|
||||||
|
⚠️ Hold back `experimental.codebase_search` (leans on indexing) and
|
||||||
|
`experimental.image_generation` (adds a tool) — arguably "enabling a feature."
|
||||||
|
|
||||||
|
## Tier 2 — Config is easy, but honoring it needs JetBrains rendering work
|
||||||
|
|
||||||
|
| Setting | Config key | Extra work |
|
||||||
|
|---|---|---|
|
||||||
|
| Auto-collapse reasoning | `auto_collapse_reasoning` | Reasoning-card default collapse |
|
||||||
|
| Terminal command display | `terminal_command_display` (expanded/collapsed) | Tool-card default state |
|
||||||
|
| Code edit display | `code_edit_display` (expanded/collapsed) | Edit-card default state |
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
Add a new **"General/Display" page + "Experimental" page** driven entirely by CLI config,
|
||||||
|
seeded with Tier 1 behavioral settings, plus wire the already-built **Claude Code compat**
|
||||||
|
toggle. This closes most of the non-feature gap with:
|
||||||
|
|
||||||
|
- zero CLI/SDK changes,
|
||||||
|
- one allowlist extension in `KiloCliDataParser.buildConfigPatch` (add keys + boolean/number serialization),
|
||||||
|
- reuse of existing `settings/base/` UI primitives and test patterns (`FakeAppRpcApi`
|
||||||
|
frontend test + `MockCliServer` backend body assertion).
|
||||||
|
|
||||||
|
Do Tier 2 (reasoning/terminal/edit display defaults) after Tier 1, since it touches the
|
||||||
|
session-rendering layer rather than being pure config.
|
||||||
+1
-1
@@ -165,6 +165,6 @@
|
|||||||
"pacote@21.5.1": "patches/pacote@21.5.1.patch",
|
"pacote@21.5.1": "patches/pacote@21.5.1.patch",
|
||||||
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch"
|
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch"
|
||||||
},
|
},
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"peerDependencies": {}
|
"peerDependencies": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -555,6 +555,7 @@ const SessionRevert = Schema.Struct({
|
|||||||
partID: optionalOmitUndefined(PartID),
|
partID: optionalOmitUndefined(PartID),
|
||||||
snapshot: optionalOmitUndefined(Schema.String),
|
snapshot: optionalOmitUndefined(Schema.String),
|
||||||
diff: optionalOmitUndefined(Schema.String),
|
diff: optionalOmitUndefined(Schema.String),
|
||||||
|
workspace: optionalOmitUndefined(Schema.Literals(["restored", "snapshots-disabled", "unavailable"])), // kilocode_change
|
||||||
})
|
})
|
||||||
|
|
||||||
const SessionModel = Schema.Struct({
|
const SessionModel = Schema.Struct({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"name": "@opencode-ai/effect-sqlite-node",
|
"name": "@opencode-ai/effect-sqlite-node",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
id = "kilo"
|
id = "kilo"
|
||||||
name = "Kilo"
|
name = "Kilo"
|
||||||
description = "The open source coding agent."
|
description = "The open source coding agent."
|
||||||
version = "7.4.11"
|
version = "7.4.13"
|
||||||
schema_version = 1
|
schema_version = 1
|
||||||
authors = ["Anomaly"]
|
authors = ["Anomaly"]
|
||||||
repository = "https://github.com/Kilo-Org/kilocode"
|
repository = "https://github.com/Kilo-Org/kilocode"
|
||||||
@@ -11,26 +11,26 @@ name = "Kilo"
|
|||||||
icon = "./icons/opencode.svg"
|
icon = "./icons/opencode.svg"
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-aarch64]
|
[agent_servers.opencode.targets.darwin-aarch64]
|
||||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.11/opencode-darwin-arm64.zip"
|
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.13/opencode-darwin-arm64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-x86_64]
|
[agent_servers.opencode.targets.darwin-x86_64]
|
||||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.11/opencode-darwin-x64.zip"
|
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.13/opencode-darwin-x64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-aarch64]
|
[agent_servers.opencode.targets.linux-aarch64]
|
||||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.11/opencode-linux-arm64.tar.gz"
|
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.13/opencode-linux-arm64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-x86_64]
|
[agent_servers.opencode.targets.linux-x86_64]
|
||||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.11/opencode-linux-x64.tar.gz"
|
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.13/opencode-linux-x64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.windows-x86_64]
|
[agent_servers.opencode.targets.windows-x86_64]
|
||||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.11/opencode-windows-x64.zip"
|
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.13/opencode-windows-x64.zip"
|
||||||
cmd = "./opencode.exe"
|
cmd = "./opencode.exe"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"name": "@opencode-ai/http-recorder",
|
"name": "@opencode-ai/http-recorder",
|
||||||
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
|
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@kilocode/kilo-console",
|
"name": "@kilocode/kilo-console",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@kilocode/kilo-docs",
|
"name": "@kilocode/kilo-docs",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --webpack --port 3002",
|
"dev": "next dev --webpack --port 3002",
|
||||||
|
|||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:6a89d1a9a31f7de1780d772b7caf71d96f0b4b895df954c77dcd508a7febe7c9
|
|
||||||
size 1950
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:431a0c12cb844b5d4e535cc4c7bf5546fd9e66a178b8023b00978d07c291a531
|
|
||||||
size 3919
|
|
||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:42c43e9ba12bc73da98c915dafca274beee1d0c893c168d8af4e3e3f41edd367
|
oid sha256:3b1eb6cf15d5c2dc66c769a91fcf09bcf25bc2e8d17696ad0e9ab4181ed3038c
|
||||||
size 630896
|
size 658647
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@kilocode/kilo-gateway",
|
"name": "@kilocode/kilo-gateway",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
|
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
|
||||||
|
|||||||
@@ -97,11 +97,6 @@ export async function fetchKiloModels(options?: {
|
|||||||
const models: Record<string, any> = {}
|
const models: Record<string, any> = {}
|
||||||
|
|
||||||
for (const model of raw.data) {
|
for (const model of raw.data) {
|
||||||
// Skip image generation models
|
|
||||||
if (model.architecture?.output_modalities?.includes("image")) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip models that don't support tools — Kilo requires tool calling
|
// Skip models that don't support tools — Kilo requires tool calling
|
||||||
if (!model.supported_parameters?.includes("tools")) {
|
if (!model.supported_parameters?.includes("tools")) {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -248,3 +248,85 @@ test("returns error with kind=schema when response body is invalid JSON", async
|
|||||||
expect(result.models).toEqual({})
|
expect(result.models).toEqual({})
|
||||||
expect(result.error?.kind).toBe("schema")
|
expect(result.error?.kind).toBe("schema")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const MIXED_MODALITY_RESPONSE = JSON.stringify({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: "openrouter/auto",
|
||||||
|
name: "Auto Router",
|
||||||
|
context_length: 2000000,
|
||||||
|
max_completion_tokens: 16384,
|
||||||
|
architecture: {
|
||||||
|
input_modalities: ["text", "image"],
|
||||||
|
output_modalities: ["text", "image"],
|
||||||
|
},
|
||||||
|
supported_parameters: ["tools", "temperature"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "openrouter/auto-beta",
|
||||||
|
name: "Auto Router (Beta)",
|
||||||
|
context_length: 2000000,
|
||||||
|
max_completion_tokens: 16384,
|
||||||
|
architecture: {
|
||||||
|
input_modalities: ["text", "image"],
|
||||||
|
output_modalities: ["text", "image"],
|
||||||
|
},
|
||||||
|
supported_parameters: ["tools", "temperature"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "black-forest-labs/flux-1.1-pro",
|
||||||
|
name: "FLUX 1.1 Pro",
|
||||||
|
context_length: 4096,
|
||||||
|
max_completion_tokens: 4096,
|
||||||
|
architecture: {
|
||||||
|
input_modalities: ["text", "image"],
|
||||||
|
output_modalities: ["image"],
|
||||||
|
},
|
||||||
|
supported_parameters: ["tools"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "test/no-tools",
|
||||||
|
name: "No Tools Model",
|
||||||
|
context_length: 128000,
|
||||||
|
max_completion_tokens: 16384,
|
||||||
|
architecture: {
|
||||||
|
input_modalities: ["text"],
|
||||||
|
output_modalities: ["text"],
|
||||||
|
},
|
||||||
|
supported_parameters: ["temperature"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "test/model-a",
|
||||||
|
name: "Test Model A",
|
||||||
|
context_length: 128000,
|
||||||
|
max_completion_tokens: 16384,
|
||||||
|
architecture: {
|
||||||
|
input_modalities: ["text"],
|
||||||
|
output_modalities: ["text"],
|
||||||
|
},
|
||||||
|
supported_parameters: ["tools", "temperature"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
test("keeps image-output models with tools and drops models without tools", async () => {
|
||||||
|
const orig = globalThis.fetch
|
||||||
|
stubFetch(
|
||||||
|
async () =>
|
||||||
|
new Response(MIXED_MODALITY_RESPONSE, {
|
||||||
|
status: 200,
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = await fetchKiloModels({})
|
||||||
|
|
||||||
|
;(globalThis as any).fetch = orig
|
||||||
|
|
||||||
|
expect(result.error).toBeUndefined()
|
||||||
|
expect(result.models["openrouter/auto"]).toBeDefined()
|
||||||
|
expect(result.models["openrouter/auto-beta"]).toBeDefined()
|
||||||
|
expect(result.models["black-forest-labs/flux-1.1-pro"]).toBeDefined()
|
||||||
|
expect(result.models["test/model-a"]).toBeDefined()
|
||||||
|
expect(result.models["test/no-tools"]).toBeUndefined()
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@kilocode/kilo-i18n",
|
"name": "@kilocode/kilo-i18n",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "Kilo-specific i18n translations and overrides",
|
"description": "Kilo-specific i18n translations and overrides",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@kilocode/kilo-indexing",
|
"name": "@kilocode/kilo-indexing",
|
||||||
"version": "7.4.11",
|
"version": "7.4.13",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "Standalone indexing engine and host helpers for Kilo Code",
|
"description": "Standalone indexing engine and host helpers for Kilo Code",
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 7.4.12
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#12191](https://github.com/Kilo-Org/kilocode/pull/12191) [`4d676b6`](https://github.com/Kilo-Org/kilocode/commit/4d676b68d2d0dd025c7d1a6684f49f3d03e9d12d) - Use Kilo Core for JetBrains @ file completion.
|
||||||
|
|
||||||
## 7.4.10
|
## 7.4.10
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
+8
@@ -820,11 +820,13 @@ class KiloBackendAppService private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"global.disposed" -> {
|
"global.disposed" -> {
|
||||||
|
logSessionDisposalRisk("global.disposed")
|
||||||
log.info("SSE global.disposed — triggering full application reload")
|
log.info("SSE global.disposed — triggering full application reload")
|
||||||
val current = _appState.value
|
val current = _appState.value
|
||||||
if (current is KiloAppState.Ready) load()
|
if (current is KiloAppState.Ready) load()
|
||||||
}
|
}
|
||||||
"server.instance.disposed" -> {
|
"server.instance.disposed" -> {
|
||||||
|
logSessionDisposalRisk("server.instance.disposed")
|
||||||
log.info("SSE server.instance.disposed — triggering full application reload")
|
log.info("SSE server.instance.disposed — triggering full application reload")
|
||||||
val current = _appState.value
|
val current = _appState.value
|
||||||
if (current is KiloAppState.Ready) load()
|
if (current is KiloAppState.Ready) load()
|
||||||
@@ -835,6 +837,12 @@ class KiloBackendAppService private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun logSessionDisposalRisk(event: String) {
|
||||||
|
val active = sessions.statuses.value.filterValues { it.type != "idle" }
|
||||||
|
if (active.isEmpty()) return
|
||||||
|
log.warn("SSE $event while sessions are active; sessions may be cancelled count=${active.size} statuses=${active.values.map { it.type }.distinct()}")
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun clear() {
|
private suspend fun clear() {
|
||||||
synchronized(loadLock) {
|
synchronized(loadLock) {
|
||||||
val jobs = listOfNotNull(loader, eventWatcher)
|
val jobs = listOfNotNull(loader, eventWatcher)
|
||||||
|
|||||||
+4
@@ -86,6 +86,10 @@ class KiloBackendSessionManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
|
val active = _statuses.value.filterValues { it.type != "idle" }
|
||||||
|
if (active.isNotEmpty()) {
|
||||||
|
log.warn("Session manager stopping with active sessions count=${active.size} statuses=${active.values.map { it.type }.distinct()}")
|
||||||
|
}
|
||||||
watcher?.cancel()
|
watcher?.cancel()
|
||||||
watcher = null
|
watcher = null
|
||||||
client = null
|
client = null
|
||||||
|
|||||||
+46
-1
@@ -20,6 +20,7 @@ import ai.kilocode.rpc.dto.CommandDto
|
|||||||
import ai.kilocode.rpc.dto.ConfigDto
|
import ai.kilocode.rpc.dto.ConfigDto
|
||||||
import ai.kilocode.rpc.dto.ConfigPatchDto
|
import ai.kilocode.rpc.dto.ConfigPatchDto
|
||||||
import ai.kilocode.rpc.dto.ConfigUpdateDto
|
import ai.kilocode.rpc.dto.ConfigUpdateDto
|
||||||
|
import ai.kilocode.rpc.dto.CompactionConfigDto
|
||||||
import ai.kilocode.rpc.dto.CustomModelDto
|
import ai.kilocode.rpc.dto.CustomModelDto
|
||||||
import ai.kilocode.rpc.dto.CustomProviderConfigDto
|
import ai.kilocode.rpc.dto.CustomProviderConfigDto
|
||||||
import ai.kilocode.rpc.dto.CustomProviderSaveDto
|
import ai.kilocode.rpc.dto.CustomProviderSaveDto
|
||||||
@@ -73,6 +74,7 @@ import ai.kilocode.rpc.dto.TodoDto
|
|||||||
import ai.kilocode.rpc.dto.TodoViewDto
|
import ai.kilocode.rpc.dto.TodoViewDto
|
||||||
import ai.kilocode.rpc.dto.TokensDto
|
import ai.kilocode.rpc.dto.TokensDto
|
||||||
import ai.kilocode.rpc.dto.ToolRefDto
|
import ai.kilocode.rpc.dto.ToolRefDto
|
||||||
|
import ai.kilocode.rpc.dto.WatcherConfigDto
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonArray
|
import kotlinx.serialization.json.JsonArray
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
@@ -521,6 +523,8 @@ object KiloCliDataParser {
|
|||||||
subagentModel = obj.str("subagent_model"),
|
subagentModel = obj.str("subagent_model"),
|
||||||
subagentVariant = obj.str("subagent_variant"),
|
subagentVariant = obj.str("subagent_variant"),
|
||||||
defaultAgent = obj.str("default_agent"),
|
defaultAgent = obj.str("default_agent"),
|
||||||
|
watcher = parseWatcherConfig(obj["watcher"].obj()),
|
||||||
|
compaction = parseCompactionConfig(obj["compaction"].obj()),
|
||||||
instructions = obj["instructions"].arr()
|
instructions = obj["instructions"].arr()
|
||||||
?.mapNotNull { runCatching { it.jsonPrimitive.contentOrNull }.getOrNull() }
|
?.mapNotNull { runCatching { it.jsonPrimitive.contentOrNull }.getOrNull() }
|
||||||
?: emptyList(),
|
?: emptyList(),
|
||||||
@@ -530,6 +534,24 @@ object KiloCliDataParser {
|
|||||||
)
|
)
|
||||||
}.getOrDefault(ConfigDto())
|
}.getOrDefault(ConfigDto())
|
||||||
|
|
||||||
|
private fun parseWatcherConfig(obj: JsonObject?): WatcherConfigDto? {
|
||||||
|
if (obj == null) return null
|
||||||
|
return WatcherConfigDto(
|
||||||
|
ignore = obj["ignore"].arr()
|
||||||
|
?.mapNotNull { runCatching { it.jsonPrimitive.contentOrNull }.getOrNull() }
|
||||||
|
?: emptyList(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseCompactionConfig(obj: JsonObject?): CompactionConfigDto? {
|
||||||
|
if (obj == null) return null
|
||||||
|
return CompactionConfigDto(
|
||||||
|
auto = runCatching { obj.flagOrNull("auto") }.getOrNull(),
|
||||||
|
threshold_percent = runCatching { obj.num("threshold_percent") }.getOrNull(),
|
||||||
|
prune = runCatching { obj.flagOrNull("prune") }.getOrNull(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseSkillsConfig(obj: JsonObject?): SkillsConfigDto? {
|
private fun parseSkillsConfig(obj: JsonObject?): SkillsConfigDto? {
|
||||||
if (obj == null) return null
|
if (obj == null) return null
|
||||||
return SkillsConfigDto(
|
return SkillsConfigDto(
|
||||||
@@ -625,7 +647,12 @@ object KiloCliDataParser {
|
|||||||
val obj = item.obj() ?: return@mapNotNull null
|
val obj = item.obj() ?: return@mapNotNull null
|
||||||
val name = obj.str("name") ?: return@mapNotNull null
|
val name = obj.str("name") ?: return@mapNotNull null
|
||||||
val location = obj.str("location") ?: return@mapNotNull null
|
val location = obj.str("location") ?: return@mapNotNull null
|
||||||
SkillDto(name = name, description = obj.str("description"), location = location)
|
SkillDto(
|
||||||
|
name = name,
|
||||||
|
description = obj.str("description"),
|
||||||
|
location = location,
|
||||||
|
content = obj.str("content"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseAgentBehaviorCommands(raw: String): List<CommandDto> =
|
fun parseAgentBehaviorCommands(raw: String): List<CommandDto> =
|
||||||
@@ -838,6 +865,24 @@ object KiloCliDataParser {
|
|||||||
val instructions = patch.instructions
|
val instructions = patch.instructions
|
||||||
if (instructions != null) put("instructions", JsonArray(instructions.map(::JsonPrimitive)))
|
if (instructions != null) put("instructions", JsonArray(instructions.map(::JsonPrimitive)))
|
||||||
|
|
||||||
|
val watcher = patch.watcher
|
||||||
|
if (watcher != null) {
|
||||||
|
put("watcher", buildJsonObject {
|
||||||
|
val ignore = watcher.ignore
|
||||||
|
if (ignore != null) put("ignore", JsonArray(ignore.map(::JsonPrimitive)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
val compaction = patch.compaction
|
||||||
|
if (compaction != null) {
|
||||||
|
put("compaction", buildJsonObject {
|
||||||
|
for (field in compaction.clear) put(field, JsonNull)
|
||||||
|
if (compaction.auto != null) put("auto", compaction.auto)
|
||||||
|
if (compaction.threshold_percent != null) put("threshold_percent", compaction.threshold_percent)
|
||||||
|
if (compaction.prune != null) put("prune", compaction.prune)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
val skills = patch.skills
|
val skills = patch.skills
|
||||||
if (skills != null) {
|
if (skills != null) {
|
||||||
put("skills", buildJsonObject {
|
put("skills", buildJsonObject {
|
||||||
|
|||||||
+159
-1
@@ -14,6 +14,7 @@ import ai.kilocode.rpc.dto.ConfigPatchDto
|
|||||||
import ai.kilocode.rpc.dto.McpConfigDto
|
import ai.kilocode.rpc.dto.McpConfigDto
|
||||||
import ai.kilocode.rpc.dto.McpServerConfigDto
|
import ai.kilocode.rpc.dto.McpServerConfigDto
|
||||||
import ai.kilocode.rpc.dto.PermissionRuleItemDto
|
import ai.kilocode.rpc.dto.PermissionRuleItemDto
|
||||||
|
import ai.kilocode.rpc.dto.SkillDto
|
||||||
import com.intellij.openapi.components.service
|
import com.intellij.openapi.components.service
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -22,7 +23,11 @@ import kotlinx.serialization.json.JsonPrimitive
|
|||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import com.intellij.openapi.util.SystemInfo
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.InvalidPathException
|
||||||
|
import java.nio.file.Path
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
@@ -33,6 +38,7 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? =
|
|||||||
private val JSON = "application/json".toMediaType()
|
private val JSON = "application/json".toMediaType()
|
||||||
private val saved = ConcurrentHashMap<String, SavedMcp>()
|
private val saved = ConcurrentHashMap<String, SavedMcp>()
|
||||||
private val port = AtomicInteger(-1)
|
private val port = AtomicInteger(-1)
|
||||||
|
private val extensions = setOf("md", "markdown", "txt", "text", "html", "htm")
|
||||||
}
|
}
|
||||||
|
|
||||||
private val app: KiloBackendAppService get() = backend ?: service()
|
private val app: KiloBackendAppService get() = backend ?: service()
|
||||||
@@ -56,11 +62,59 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun skills(directory: String) = KiloCliDataParser.parseAgentBehaviorSkills(request(directory, "/skill", null))
|
override suspend fun skills(directory: String): List<SkillDto> {
|
||||||
|
val items = KiloCliDataParser.parseAgentBehaviorSkills(request(directory, "/skill", null))
|
||||||
|
return items.map { item ->
|
||||||
|
val editable = editable(item)
|
||||||
|
item.copy(content = skillContent(item) ?: item.content, editable = editable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun removeSkill(directory: String, location: String): Boolean =
|
override suspend fun removeSkill(directory: String, location: String): Boolean =
|
||||||
post(directory, "/kilocode/skill/remove", JsonObject(mapOf("location" to JsonPrimitive(location))))
|
post(directory, "/kilocode/skill/remove", JsonObject(mapOf("location" to JsonPrimitive(location))))
|
||||||
|
|
||||||
|
override suspend fun reloadSkills(directory: String): Boolean {
|
||||||
|
LOG.info("Skills reload requested dir=$directory")
|
||||||
|
if (hasActiveSession(directory)) {
|
||||||
|
LOG.warn("Skills reload blocked by active session dir=$directory")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
runCatching { post(directory, "/instance/reload") }.onFailure { err ->
|
||||||
|
LOG.warn("Skills reload failed dir=$directory", err)
|
||||||
|
}.getOrThrow()
|
||||||
|
LOG.info("Skills reload succeeded dir=$directory")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun saveSkill(directory: String, location: String, content: String): Boolean {
|
||||||
|
LOG.info("Skill save requested dir=$directory location=$location")
|
||||||
|
app.requireReady()
|
||||||
|
val paths = knownSkills(directory)
|
||||||
|
val path = writablePath(directory, location, paths) ?: return false
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
Files.writeString(path, content, StandardCharsets.UTF_8)
|
||||||
|
}
|
||||||
|
LOG.info("Skill file saved dir=$directory path=$path bytes=${content.toByteArray(StandardCharsets.UTF_8).size}")
|
||||||
|
LOG.info("Skill save reload deferred dir=$directory path=$path")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun saveSkills(directory: String, edits: Map<String, String>): Boolean {
|
||||||
|
LOG.info("Skills save requested dir=$directory count=${edits.size}")
|
||||||
|
app.requireReady()
|
||||||
|
val known = knownSkills(directory)
|
||||||
|
val paths = edits.mapNotNull { (location, content) ->
|
||||||
|
val path = writablePath(directory, location, known) ?: return false
|
||||||
|
path to content
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
for ((path, content) in paths) Files.writeString(path, content, StandardCharsets.UTF_8)
|
||||||
|
}
|
||||||
|
LOG.info("Skill files saved dir=$directory count=${paths.size}")
|
||||||
|
LOG.info("Skills save reload deferred dir=$directory count=${paths.size}")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun removeAgent(directory: String, name: String): Boolean =
|
override suspend fun removeAgent(directory: String, name: String): Boolean =
|
||||||
post(directory, "/kilocode/agent/remove", JsonObject(mapOf("name" to JsonPrimitive(name))))
|
post(directory, "/kilocode/agent/remove", JsonObject(mapOf("name" to JsonPrimitive(name))))
|
||||||
|
|
||||||
@@ -139,6 +193,104 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? =
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun hasActiveSession(directory: String): Boolean {
|
||||||
|
val active = app.sessions.statuses.value.filterValues { it.type != "idle" }
|
||||||
|
if (active.isNotEmpty()) {
|
||||||
|
LOG.info("Skills reload active statuses dir=$directory count=${active.size} types=${active.values.map { it.type }.distinct()}")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val permissions = runCatching { app.chat.pendingPermissions(directory) }.onFailure { err ->
|
||||||
|
LOG.warn("Skills reload pending permission check failed dir=$directory", err)
|
||||||
|
}.getOrDefault(emptyList())
|
||||||
|
if (permissions.isNotEmpty()) {
|
||||||
|
LOG.info("Skills reload pending permissions dir=$directory count=${permissions.size}")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val questions = runCatching { app.chat.pendingQuestions(directory) }.onFailure { err ->
|
||||||
|
LOG.warn("Skills reload pending question check failed dir=$directory", err)
|
||||||
|
}.getOrDefault(emptyList())
|
||||||
|
if (questions.isNotEmpty()) {
|
||||||
|
LOG.info("Skills reload pending questions dir=$directory count=${questions.size}")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun skillContent(skill: SkillDto): String? {
|
||||||
|
val path = resolveSkillPath(skill.location) ?: return null
|
||||||
|
return runCatching {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
if (!Files.isRegularFile(path)) null else Files.readString(path, StandardCharsets.UTF_8)
|
||||||
|
}
|
||||||
|
}.onFailure { err ->
|
||||||
|
LOG.warn("Skill content read failed: $path", err)
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun editable(skill: SkillDto): Boolean {
|
||||||
|
val path = resolveSkillPath(skill.location) ?: return false
|
||||||
|
if (urlCached(path)) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun knownSkills(directory: String): Set<Path> {
|
||||||
|
val items = KiloCliDataParser.parseAgentBehaviorSkills(request(directory, "/skill", null))
|
||||||
|
return items.mapNotNull { item -> resolveEditablePath(item) }.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writablePath(directory: String, location: String, known: Set<Path>): Path? {
|
||||||
|
val path = resolveSkillPath(location)
|
||||||
|
if (path == null) {
|
||||||
|
LOG.warn("Skill save rejected: invalid location dir=$directory location=$location")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (path !in known) {
|
||||||
|
LOG.warn("Skill save rejected: unknown skill dir=$directory path=$path")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveEditablePath(skill: SkillDto): Path? {
|
||||||
|
val path = resolveSkillPath(skill.location) ?: return null
|
||||||
|
if (urlCached(path)) return null
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveSkillPath(location: String): Path? {
|
||||||
|
val raw = normalizeWorkspacePath(location) ?: return null
|
||||||
|
val path = try {
|
||||||
|
Path.of(raw).normalize()
|
||||||
|
} catch (_: InvalidPathException) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (!path.isAbsolute || !isSkillFile(path)) return null
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun urlCached(path: Path): Boolean {
|
||||||
|
return cacheRoots().any { root -> path.startsWith(root.resolve("kilo").resolve("skills").normalize()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cacheRoots(): Set<Path> = buildSet {
|
||||||
|
val home = System.getProperty("user.home")
|
||||||
|
add(Path.of(cacheRoot()).normalize())
|
||||||
|
add(Path.of(home, ".cache").normalize())
|
||||||
|
add(Path.of(home, "Library", "Caches").normalize())
|
||||||
|
System.getenv("LOCALAPPDATA")?.takeIf { it.isNotBlank() }?.let { add(Path.of(it).normalize()) }
|
||||||
|
add(Path.of(home, "AppData", "Local").normalize())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cacheRoot(): String {
|
||||||
|
val xdg = System.getenv("XDG_CACHE_HOME")?.takeIf { it.isNotBlank() }
|
||||||
|
if (xdg != null) return xdg
|
||||||
|
val home = System.getProperty("user.home")
|
||||||
|
if (SystemInfo.isMac) return Path.of(home, "Library", "Caches").toString()
|
||||||
|
if (SystemInfo.isWindows) return System.getenv("LOCALAPPDATA")?.takeIf { it.isNotBlank() }
|
||||||
|
?: Path.of(home, "AppData", "Local").toString()
|
||||||
|
return Path.of(home, ".cache").toString()
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun patchConfig(path: String, body: String): Unit = withContext(Dispatchers.IO) {
|
private suspend fun patchConfig(path: String, body: String): Unit = withContext(Dispatchers.IO) {
|
||||||
val http = app.http ?: throw IllegalStateException("Kilo HTTP client is unavailable")
|
val http = app.http ?: throw IllegalStateException("Kilo HTTP client is unavailable")
|
||||||
val url = "http://127.0.0.1:${app.port}$path"
|
val url = "http://127.0.0.1:${app.port}$path"
|
||||||
@@ -234,6 +386,12 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? =
|
|||||||
|
|
||||||
private fun encodePath(value: String): String = encode(value).replace("+", "%20")
|
private fun encodePath(value: String): String = encode(value).replace("+", "%20")
|
||||||
|
|
||||||
|
private fun isSkillFile(path: Path): Boolean {
|
||||||
|
val name = path.fileName?.toString() ?: return false
|
||||||
|
if (name == "SKILL.md") return true
|
||||||
|
return name.substringAfterLast('.', "").lowercase() in extensions
|
||||||
|
}
|
||||||
|
|
||||||
private data class SavedMcp(
|
private data class SavedMcp(
|
||||||
val directory: String,
|
val directory: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
|
|||||||
+2
@@ -64,6 +64,8 @@ internal object KiloWorkspaceDtoMapper {
|
|||||||
name = s.name,
|
name = s.name,
|
||||||
description = s.description,
|
description = s.description,
|
||||||
location = s.location,
|
location = s.location,
|
||||||
|
content = s.content,
|
||||||
|
editable = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun provider(p: ProviderInfo) = ProviderDto(
|
private fun provider(p: ProviderInfo) = ProviderDto(
|
||||||
|
|||||||
+57
-103
@@ -22,33 +22,22 @@ import ai.kilocode.rpc.dto.ModelsWorkspaceDto
|
|||||||
import ai.kilocode.rpc.dto.WorkspaceFileDto
|
import ai.kilocode.rpc.dto.WorkspaceFileDto
|
||||||
import com.intellij.execution.configurations.GeneralCommandLine
|
import com.intellij.execution.configurations.GeneralCommandLine
|
||||||
import com.intellij.execution.process.CapturingProcessHandler
|
import com.intellij.execution.process.CapturingProcessHandler
|
||||||
import com.intellij.ide.actions.searcheverywhere.FoundItemDescriptor
|
|
||||||
import com.intellij.ide.util.gotoByName.ChooseByNameInScopeItemProvider
|
|
||||||
import com.intellij.ide.util.gotoByName.ChooseByNamePopup
|
|
||||||
import com.intellij.ide.util.gotoByName.ChooseByNameViewModel
|
|
||||||
import com.intellij.ide.util.gotoByName.GotoFileModel
|
|
||||||
import com.intellij.openapi.application.ApplicationManager
|
import com.intellij.openapi.application.ApplicationManager
|
||||||
import com.intellij.openapi.application.ModalityState
|
import com.intellij.openapi.application.ModalityState
|
||||||
import com.intellij.openapi.application.readAction
|
|
||||||
import com.intellij.openapi.components.service
|
import com.intellij.openapi.components.service
|
||||||
import com.intellij.openapi.progress.EmptyProgressIndicator
|
|
||||||
import com.intellij.openapi.project.DumbService
|
|
||||||
import com.intellij.openapi.project.IndexNotReadyException
|
|
||||||
import com.intellij.openapi.fileEditor.OpenFileDescriptor
|
import com.intellij.openapi.fileEditor.OpenFileDescriptor
|
||||||
import com.intellij.openapi.project.Project
|
import com.intellij.openapi.project.Project
|
||||||
import com.intellij.openapi.project.ProjectManager
|
import com.intellij.openapi.project.ProjectManager
|
||||||
import com.intellij.openapi.roots.ProjectFileIndex
|
|
||||||
import com.intellij.openapi.util.io.FileUtil
|
import com.intellij.openapi.util.io.FileUtil
|
||||||
import com.intellij.openapi.vfs.VirtualFile
|
import com.intellij.openapi.vfs.VirtualFile
|
||||||
import com.intellij.openapi.vfs.LocalFileSystem
|
import com.intellij.openapi.vfs.LocalFileSystem
|
||||||
import com.intellij.platform.project.ProjectId
|
import com.intellij.platform.project.ProjectId
|
||||||
import com.intellij.platform.project.findProjectOrNull
|
import com.intellij.platform.project.findProjectOrNull
|
||||||
import com.intellij.navigation.NavigationItem
|
import kotlinx.coroutines.CancellationException
|
||||||
import com.intellij.psi.PsiFileSystemItem
|
|
||||||
import com.intellij.psi.search.GlobalSearchScope
|
|
||||||
import com.intellij.util.indexing.FindSymbolParameters
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
@@ -56,6 +45,8 @@ import kotlinx.coroutines.flow.flowOf
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
@@ -74,7 +65,9 @@ import kotlin.coroutines.resume
|
|||||||
* for the given directory. Project lookup is only used to resolve the
|
* for the given directory. Project lookup is only used to resolve the
|
||||||
* calling frontend project to the correct backend directory.
|
* calling frontend project to the correct backend directory.
|
||||||
*/
|
*/
|
||||||
class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi {
|
class KiloWorkspaceRpcApiImpl internal constructor(
|
||||||
|
private val svc: KiloBackendAppService? = null,
|
||||||
|
) : KiloWorkspaceRpcApi {
|
||||||
companion object {
|
companion object {
|
||||||
private val LOG = KiloLog.create(KiloWorkspaceRpcApiImpl::class.java)
|
private val LOG = KiloLog.create(KiloWorkspaceRpcApiImpl::class.java)
|
||||||
private const val SCHEMA = "https://app.kilo.ai/config.json"
|
private const val SCHEMA = "https://app.kilo.ai/config.json"
|
||||||
@@ -82,15 +75,15 @@ class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi {
|
|||||||
private val LEGACY = listOf("opencode.jsonc", "opencode.json")
|
private val LEGACY = listOf("opencode.jsonc", "opencode.json")
|
||||||
private val GLOBAL = MODERN + LEGACY + "config.json"
|
private val GLOBAL = MODERN + LEGACY + "config.json"
|
||||||
private val LOCAL_DIRS = listOf(".kilo", ".kilocode", ".opencode")
|
private val LOCAL_DIRS = listOf(".kilo", ".kilocode", ".opencode")
|
||||||
private const val SEARCH_CAP = 2_000
|
|
||||||
private const val DIFF_CAP = 200_000
|
private const val DIFF_CAP = 200_000
|
||||||
|
private val JSON = Json { ignoreUnknownKeys = true }
|
||||||
private val CONFIG = """{
|
private val CONFIG = """{
|
||||||
"${'$'}schema": "$SCHEMA"
|
"${'$'}schema": "$SCHEMA"
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
private val app: KiloBackendAppService get() = service()
|
private val app: KiloBackendAppService get() = svc ?: service()
|
||||||
|
|
||||||
private val gitCache = ConcurrentHashMap<String, Boolean>()
|
private val gitCache = ConcurrentHashMap<String, Boolean>()
|
||||||
|
|
||||||
@@ -196,19 +189,56 @@ class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi {
|
|||||||
override suspend fun searchFiles(directory: String, query: String, limit: Int): FileSearchResultDto {
|
override suspend fun searchFiles(directory: String, query: String, limit: Int): FileSearchResultDto {
|
||||||
val base = file(clean(directory) ?: directory) ?: return FileSearchResultDto()
|
val base = file(clean(directory) ?: directory) ?: return FileSearchResultDto()
|
||||||
val git = withContext(Dispatchers.IO) { gitAvailable(base) }
|
val git = withContext(Dispatchers.IO) { gitAvailable(base) }
|
||||||
val project = project(base) ?: return FileSearchResultDto(git = git)
|
LOG.debug { "workspace file search directory=$directory query=$query limit=$limit" }
|
||||||
if (DumbService.getInstance(project).isDumb) return FileSearchResultDto(indexing = true, git = git)
|
return searchKilo(directory, query, limit, git)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun searchKilo(directory: String, query: String, limit: Int, git: Boolean): FileSearchResultDto {
|
||||||
return try {
|
return try {
|
||||||
val files = readAction { search(project, base, query, limit.coerceIn(1, 200)) }
|
val cap = limit.coerceIn(1, 200)
|
||||||
FileSearchResultDto(files = files, git = git)
|
val (files, dirs) = coroutineScope {
|
||||||
} catch (e: IndexNotReadyException) {
|
val files = async { kiloResults(directory, query, "file", cap, false) }
|
||||||
FileSearchResultDto(indexing = true, git = git)
|
val dirs = async { kiloResults(directory, query, "directory", cap, true) }
|
||||||
} catch (e: LinkageError) {
|
files.await() to dirs.await()
|
||||||
LOG.warn("file search API unavailable; returning no suggestions", e)
|
}
|
||||||
|
val found = linkedMapOf<String, WorkspaceFileDto>()
|
||||||
|
(dirs + files).forEach { file -> found.putIfAbsent(file.path, file) }
|
||||||
|
FileSearchResultDto(files = found.values.take(cap), git = git)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LOG.warn("Kilo Core file search failed for directory=$directory query=$query", e)
|
||||||
FileSearchResultDto(git = git)
|
FileSearchResultDto(git = git)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun kiloResults(
|
||||||
|
directory: String,
|
||||||
|
query: String,
|
||||||
|
type: String,
|
||||||
|
limit: Int,
|
||||||
|
dir: Boolean,
|
||||||
|
): List<WorkspaceFileDto> {
|
||||||
|
val http = app.http ?: throw IllegalStateException("Kilo HTTP client is unavailable")
|
||||||
|
val raw = withContext(Dispatchers.IO) {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("http://127.0.0.1:${app.port}/find/file?directory=${encode(directory)}&query=${encode(query)}&type=$type&limit=$limit")
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
http.newCall(request).execute().use { response ->
|
||||||
|
val body = response.body?.string().orEmpty()
|
||||||
|
if (!response.isSuccessful) throw RuntimeException("HTTP ${response.code}: $body")
|
||||||
|
body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return JSON.decodeFromString<List<String>>(raw)
|
||||||
|
.asSequence()
|
||||||
|
.map { it.trimEnd('/') }
|
||||||
|
.filter { it.isNotBlank() && !isManagedWorktreeStorage(it) }
|
||||||
|
.map { WorkspaceFileDto(it, it.substringAfterLast('/'), dir) }
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun gitChanges(directory: String): String? = withContext(Dispatchers.IO) {
|
override suspend fun gitChanges(directory: String): String? = withContext(Dispatchers.IO) {
|
||||||
val base = file(clean(directory) ?: directory) ?: return@withContext null
|
val base = file(clean(directory) ?: directory) ?: return@withContext null
|
||||||
if (!gitAvailable(base)) return@withContext null
|
if (!gitAvailable(base)) return@withContext null
|
||||||
@@ -311,10 +341,11 @@ class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi {
|
|||||||
}
|
}
|
||||||
descriptor.navigate(true)
|
descriptor.navigate(true)
|
||||||
if (cont.isActive) cont.resume(Unit)
|
if (cont.isActive) cont.resume(Unit)
|
||||||
}, ModalityState.any())
|
}, ModalityState.nonModal())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun project(path: Path): Project? {
|
private fun project(path: Path): Project? {
|
||||||
|
if (ApplicationManager.getApplication() == null) return null
|
||||||
val projects = ProjectManager.getInstance().openProjects.filter { !it.isDefault }
|
val projects = ProjectManager.getInstance().openProjects.filter { !it.isDefault }
|
||||||
return projects.firstOrNull { item ->
|
return projects.firstOrNull { item ->
|
||||||
val base = item.basePath?.let(::file) ?: return@firstOrNull false
|
val base = item.basePath?.let(::file) ?: return@firstOrNull false
|
||||||
@@ -322,83 +353,6 @@ class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi {
|
|||||||
} ?: projects.firstOrNull()
|
} ?: projects.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uses the IDE Go-to-File engine (com.intellij.ide.util.gotoByName.*). These are public but
|
|
||||||
// unstable lang-impl classes (not @ApiStatus.Internal) -- the same engine behind Search Everywhere,
|
|
||||||
// chosen for proven large-repo performance. searchFiles() degrades gracefully on LinkageError.
|
|
||||||
@Suppress("UnstableApiUsage")
|
|
||||||
private fun search(project: Project, base: Path, query: String, limit: Int): List<WorkspaceFileDto> {
|
|
||||||
val text = query.trim()
|
|
||||||
if (text.isBlank()) return roots(project, base, limit)
|
|
||||||
val scope = GlobalSearchScope.projectScope(project)
|
|
||||||
val model = object : GotoFileModel(project) {
|
|
||||||
override fun acceptItem(item: NavigationItem): Boolean {
|
|
||||||
val psi = item as? PsiFileSystemItem ?: return false
|
|
||||||
val path = file(psi.virtualFile.path) ?: return false
|
|
||||||
return relativeWithinWorkspace(base, path) != null && super.acceptItem(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun loadInitialCheckBoxState(): Boolean = false
|
|
||||||
|
|
||||||
override fun saveInitialCheckBoxState(state: Boolean) {}
|
|
||||||
}
|
|
||||||
val view = object : ChooseByNameViewModel {
|
|
||||||
override fun getProject(): Project = project
|
|
||||||
|
|
||||||
override fun getModel() = model
|
|
||||||
|
|
||||||
override fun isSearchInAnyPlace(): Boolean = model.useMiddleMatching()
|
|
||||||
|
|
||||||
override fun transformPattern(pattern: String): String = ChooseByNamePopup.getTransformedPattern(pattern, model)
|
|
||||||
|
|
||||||
override fun canShowListForEmptyPattern(): Boolean = false
|
|
||||||
|
|
||||||
override fun getMaximumListSizeLimit(): Int = limit
|
|
||||||
}
|
|
||||||
val provider = model.getItemProvider(null)
|
|
||||||
val params = FindSymbolParameters.wrap(text, scope)
|
|
||||||
val found = mutableListOf<FoundItemDescriptor<*>>()
|
|
||||||
val indicator = EmptyProgressIndicator()
|
|
||||||
if (provider is ChooseByNameInScopeItemProvider) {
|
|
||||||
provider.filterElementsWithWeights(view, params, indicator) { item ->
|
|
||||||
found += item
|
|
||||||
found.size < SEARCH_CAP
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
provider.filterElements(view, text, false, indicator) { item ->
|
|
||||||
found += FoundItemDescriptor(item, 0)
|
|
||||||
found.size < SEARCH_CAP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return found.asSequence()
|
|
||||||
.sortedByDescending { it.weight }
|
|
||||||
.mapNotNull { item -> (item.item as? PsiFileSystemItem)?.virtualFile }
|
|
||||||
.mapNotNull { vf -> fileDto(base, vf) }
|
|
||||||
.distinctBy { it.path }
|
|
||||||
.take(limit)
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun roots(project: Project, base: Path, limit: Int): List<WorkspaceFileDto> {
|
|
||||||
val root = LocalFileSystem.getInstance().refreshAndFindFileByNioFile(base) ?: return emptyList()
|
|
||||||
val index = ProjectFileIndex.getInstance(project)
|
|
||||||
return root.children.asSequence()
|
|
||||||
.filter { it.name != ".git" }
|
|
||||||
.filterNot { index.isExcluded(it) }
|
|
||||||
.mapNotNull { fileDto(base, it) }
|
|
||||||
.sortedWith(
|
|
||||||
compareByDescending<WorkspaceFileDto> { it.directory }
|
|
||||||
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
|
|
||||||
)
|
|
||||||
.take(limit)
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fileDto(base: Path, vf: VirtualFile): WorkspaceFileDto? {
|
|
||||||
val path = file(vf.path) ?: return null
|
|
||||||
val rel = relativeWithinWorkspace(base, path) ?: return null
|
|
||||||
return WorkspaceFileDto(rel, vf.name, vf.isDirectory)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun gitAvailable(base: Path): Boolean {
|
private fun gitAvailable(base: Path): Boolean {
|
||||||
return workspaceGitAvailable(base, gitCache)
|
return workspaceGitAvailable(base, gitCache)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -254,6 +254,7 @@ class KiloBackendWorkspace(
|
|||||||
name = s.name,
|
name = s.name,
|
||||||
description = s.description,
|
description = s.description,
|
||||||
location = s.location,
|
location = s.location,
|
||||||
|
content = s.content,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
|
|||||||
+1
@@ -143,4 +143,5 @@ data class SkillInfo(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
val location: String,
|
val location: String,
|
||||||
|
val content: String?,
|
||||||
)
|
)
|
||||||
|
|||||||
+24
@@ -9,7 +9,9 @@ import ai.kilocode.backend.testing.FakeCliServer
|
|||||||
import ai.kilocode.backend.testing.MockCliServer
|
import ai.kilocode.backend.testing.MockCliServer
|
||||||
import ai.kilocode.backend.testing.TestLog
|
import ai.kilocode.backend.testing.TestLog
|
||||||
import ai.kilocode.rpc.dto.AgentConfigPatchDto
|
import ai.kilocode.rpc.dto.AgentConfigPatchDto
|
||||||
|
import ai.kilocode.rpc.dto.CompactionPatchDto
|
||||||
import ai.kilocode.rpc.dto.ConfigPatchDto
|
import ai.kilocode.rpc.dto.ConfigPatchDto
|
||||||
|
import ai.kilocode.rpc.dto.WatcherPatchDto
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -222,6 +224,28 @@ class KiloBackendAppServiceTest {
|
|||||||
assertEquals("fast", svc.config?.agent?.get("code")?.variant)
|
assertEquals("fast", svc.config?.agent?.get("code")?.variant)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `update config patches context settings and reloads`() = runBlocking {
|
||||||
|
val svc = create()
|
||||||
|
svc.connect()
|
||||||
|
ready(svc)
|
||||||
|
|
||||||
|
val state = svc.updateConfig(ConfigPatchDto(
|
||||||
|
watcher = WatcherPatchDto(ignore = listOf("**/dist/**", "tmp/**")),
|
||||||
|
compaction = CompactionPatchDto(auto = false, threshold_percent = 75.5, prune = false),
|
||||||
|
))
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"{\"watcher\":{\"ignore\":[\"**/dist/**\",\"tmp/**\"]},\"compaction\":{\"auto\":false,\"threshold_percent\":75.5,\"prune\":false}}",
|
||||||
|
mock.lastConfigPatchBody,
|
||||||
|
)
|
||||||
|
val cfg = appStateDto(state).config
|
||||||
|
assertEquals(listOf("**/dist/**", "tmp/**"), cfg?.watcher?.ignore)
|
||||||
|
assertEquals(false, cfg?.compaction?.auto)
|
||||||
|
assertEquals(75.5, cfg?.compaction?.threshold_percent)
|
||||||
|
assertEquals(false, svc.config?.compaction?.prune)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ready dto maps model config`() = runBlocking {
|
fun `ready dto maps model config`() = runBlocking {
|
||||||
mock.config = """{"model":"openai/gpt","agent":{"plan":{"model":"anthropic/claude","variant":"high"}}}"""
|
mock.config = """{"model":"openai/gpt","agent":{"plan":{"model":"anthropic/claude","variant":"high"}}}"""
|
||||||
|
|||||||
+11
-3
@@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.toList
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.sse.EventSource
|
import okhttp3.sse.EventSource
|
||||||
import okhttp3.sse.EventSourceListener
|
import okhttp3.sse.EventSourceListener
|
||||||
@@ -36,6 +37,10 @@ import kotlin.test.assertTrue
|
|||||||
|
|
||||||
class KiloConnectionServiceTest {
|
class KiloConnectionServiceTest {
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val WAIT_MS = 15_000L
|
||||||
|
}
|
||||||
|
|
||||||
private val mock = MockCliServer()
|
private val mock = MockCliServer()
|
||||||
private val fake = FakeCliServer(mock)
|
private val fake = FakeCliServer(mock)
|
||||||
private val log = TestLog()
|
private val log = TestLog()
|
||||||
@@ -101,20 +106,23 @@ class KiloConnectionServiceTest {
|
|||||||
val svc = KiloConnectionService(scope, server, {}, log)
|
val svc = KiloConnectionService(scope, server, {}, log)
|
||||||
val job = scope.launch { svc.connect() }
|
val job = scope.launch { svc.connect() }
|
||||||
|
|
||||||
val downloading = withTimeout(5_000) {
|
val downloading = withTimeout(WAIT_MS) {
|
||||||
svc.state.first { it is ConnectionState.Downloading }
|
svc.state.first { it is ConnectionState.Downloading }
|
||||||
}
|
}
|
||||||
assertEquals(ConnectionState.Downloading(42, "1.2.3", "darwin-arm64"), downloading)
|
assertEquals(ConnectionState.Downloading(42, "1.2.3", "darwin-arm64"), downloading)
|
||||||
|
|
||||||
resolved.complete(Unit)
|
resolved.complete(Unit)
|
||||||
withTimeout(5_000) {
|
withTimeout(WAIT_MS) {
|
||||||
svc.state.first { it == ConnectionState.Connecting }
|
svc.state.first { it == ConnectionState.Connecting }
|
||||||
}
|
}
|
||||||
|
|
||||||
ready.complete(Unit)
|
ready.complete(Unit)
|
||||||
withTimeout(5_000) {
|
val connected = withTimeoutOrNull(WAIT_MS) {
|
||||||
svc.state.first { it is ConnectionState.Connected }
|
svc.state.first { it is ConnectionState.Connected }
|
||||||
}
|
}
|
||||||
|
if (connected == null) {
|
||||||
|
error("Timed out waiting for Connected after CLI ready; state=${svc.state.value}; logs=${log.messages.joinToString("\n")}")
|
||||||
|
}
|
||||||
job.join()
|
job.join()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+57
@@ -4,6 +4,7 @@ import ai.kilocode.backend.workspace.CommandInfo
|
|||||||
import ai.kilocode.backend.workspace.ProviderData
|
import ai.kilocode.backend.workspace.ProviderData
|
||||||
import ai.kilocode.rpc.dto.ChatEventDto
|
import ai.kilocode.rpc.dto.ChatEventDto
|
||||||
import ai.kilocode.rpc.dto.AgentConfigPatchDto
|
import ai.kilocode.rpc.dto.AgentConfigPatchDto
|
||||||
|
import ai.kilocode.rpc.dto.CompactionPatchDto
|
||||||
import ai.kilocode.rpc.dto.ConfigDto
|
import ai.kilocode.rpc.dto.ConfigDto
|
||||||
import ai.kilocode.rpc.dto.ConfigPatchDto
|
import ai.kilocode.rpc.dto.ConfigPatchDto
|
||||||
import ai.kilocode.rpc.dto.ConfigUpdateDto
|
import ai.kilocode.rpc.dto.ConfigUpdateDto
|
||||||
@@ -19,6 +20,7 @@ import ai.kilocode.rpc.dto.PromptDto
|
|||||||
import ai.kilocode.rpc.dto.PromptPartDto
|
import ai.kilocode.rpc.dto.PromptPartDto
|
||||||
import ai.kilocode.rpc.dto.QuestionReplyDto
|
import ai.kilocode.rpc.dto.QuestionReplyDto
|
||||||
import ai.kilocode.rpc.dto.SkillsPatchDto
|
import ai.kilocode.rpc.dto.SkillsPatchDto
|
||||||
|
import ai.kilocode.rpc.dto.WatcherPatchDto
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
@@ -1173,6 +1175,38 @@ class KiloCliDataParserTest {
|
|||||||
assertEquals(listOf("https://example.test/skill.md"), cfg.skills?.urls)
|
assertEquals(listOf("https://example.test/skill.md"), cfg.skills?.urls)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parseConfig - context settings`() {
|
||||||
|
val cfg = KiloCliDataParser.parseConfig(
|
||||||
|
"""{
|
||||||
|
"watcher":{"ignore":["**/dist/**","tmp/**"]},
|
||||||
|
"compaction":{"auto":true,"threshold_percent":75.5,"prune":false}
|
||||||
|
}"""
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(listOf("**/dist/**", "tmp/**"), cfg.watcher?.ignore)
|
||||||
|
assertEquals(true, cfg.compaction?.auto)
|
||||||
|
assertEquals(75.5, cfg.compaction?.threshold_percent)
|
||||||
|
assertEquals(false, cfg.compaction?.prune)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parseConfig - malformed compaction fields do not discard config`() {
|
||||||
|
val cfg = KiloCliDataParser.parseConfig(
|
||||||
|
"""{
|
||||||
|
"model":"openai/gpt",
|
||||||
|
"watcher":{"ignore":["tmp/**"]},
|
||||||
|
"compaction":{"auto":{},"threshold_percent":[],"prune":false}
|
||||||
|
}"""
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals("openai/gpt", cfg.model)
|
||||||
|
assertEquals(listOf("tmp/**"), cfg.watcher?.ignore)
|
||||||
|
assertNull(cfg.compaction?.auto)
|
||||||
|
assertNull(cfg.compaction?.threshold_percent)
|
||||||
|
assertEquals(false, cfg.compaction?.prune)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `parseConfig - agent overrides and permissions`() {
|
fun `parseConfig - agent overrides and permissions`() {
|
||||||
val cfg = KiloCliDataParser.parseConfig(
|
val cfg = KiloCliDataParser.parseConfig(
|
||||||
@@ -2123,6 +2157,29 @@ class KiloCliDataParserTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `buildConfigPatch - context watcher and compaction fields`() {
|
||||||
|
val patch = ConfigPatchDto(
|
||||||
|
watcher = WatcherPatchDto(ignore = listOf("**/dist/**", "tmp/**")),
|
||||||
|
compaction = CompactionPatchDto(auto = false, threshold_percent = 75.5, prune = false),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"{\"watcher\":{\"ignore\":[\"**/dist/**\",\"tmp/**\"]},\"compaction\":{\"auto\":false,\"threshold_percent\":75.5,\"prune\":false}}",
|
||||||
|
KiloCliDataParser.buildConfigPatch(patch),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `buildConfigPatch - context threshold clear emits null`() {
|
||||||
|
val patch = ConfigPatchDto(compaction = CompactionPatchDto(clear = listOf("threshold_percent")))
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"{\"compaction\":{\"threshold_percent\":null}}",
|
||||||
|
KiloCliDataParser.buildConfigPatch(patch),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `buildConfigPatch - mcp upsert and delete`() {
|
fun `buildConfigPatch - mcp upsert and delete`() {
|
||||||
val patch = ConfigPatchDto(mcp = linkedMapOf(
|
val patch = ConfigPatchDto(mcp = linkedMapOf(
|
||||||
|
|||||||
+152
@@ -14,6 +14,8 @@ import kotlinx.coroutines.cancel
|
|||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
import kotlin.test.AfterTest
|
import kotlin.test.AfterTest
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertContains
|
import kotlin.test.assertContains
|
||||||
@@ -78,6 +80,156 @@ class KiloAgentBehaviorRpcApiImplTest {
|
|||||||
assertContains(err.message.orEmpty(), "HTTP 400")
|
assertContains(err.message.orEmpty(), "HTTP 400")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `skills and remove skill call CLI endpoints`() = runBlocking {
|
||||||
|
val dir = Files.createTempDirectory("kilo-skill-test")
|
||||||
|
val file = Files.createDirectories(dir.resolve("plan")).resolve("SKILL.md")
|
||||||
|
val content = """---
|
||||||
|
|name: plan
|
||||||
|
|description: Plan work
|
||||||
|
|---
|
||||||
|
|
|
||||||
|
|# Fresh Plan
|
||||||
|
""".trimMargin()
|
||||||
|
Files.writeString(file, content)
|
||||||
|
mock.skills = """[
|
||||||
|
{"name":"plan","description":"Plan work","location":"$file","content":"# Stale Plan"},
|
||||||
|
{"name":"builtin","location":"builtin"}
|
||||||
|
]""".trimIndent()
|
||||||
|
val rpc = rpc()
|
||||||
|
|
||||||
|
val skills = rpc.skills("/test project")
|
||||||
|
assertEquals(listOf("plan", "builtin"), skills.map { it.name })
|
||||||
|
assertEquals("Plan work", skills.single { it.name == "plan" }.description)
|
||||||
|
assertEquals(content, skills.single { it.name == "plan" }.content)
|
||||||
|
assertEquals(true, skills.single { it.name == "plan" }.editable)
|
||||||
|
assertEquals(false, skills.single { it.name == "builtin" }.editable)
|
||||||
|
|
||||||
|
assertTrue(rpc.removeSkill("/test project", file.toString()))
|
||||||
|
assertEquals("{\"location\":\"$file\"}", mock.lastSkillRemoveBody)
|
||||||
|
assertEquals(1, mock.requestCount("/kilocode/skill/remove"))
|
||||||
|
|
||||||
|
mock.skillRemoveStatus = 400
|
||||||
|
val err = assertFailsWith<RuntimeException> {
|
||||||
|
rpc.removeSkill("/test", "/tmp/missing/SKILL.md")
|
||||||
|
}
|
||||||
|
assertContains(err.message.orEmpty(), "HTTP 400")
|
||||||
|
|
||||||
|
assertTrue(rpc.reloadSkills("/test project"))
|
||||||
|
assertEquals(1, mock.requestCount("/instance/reload"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `url cached skills are read only`() = runBlocking {
|
||||||
|
val cache = Path.of(System.getProperty("user.home"), ".cache", "kilo", "skills", "remote")
|
||||||
|
val file = Files.createDirectories(cache).resolve("SKILL.md")
|
||||||
|
Files.writeString(file, "# Remote")
|
||||||
|
mock.skills = """[
|
||||||
|
{"name":"remote","description":"Remote","location":"$file","content":"# Remote"}
|
||||||
|
]""".trimIndent()
|
||||||
|
|
||||||
|
val skill = rpc().skills("/test project").single()
|
||||||
|
|
||||||
|
assertEquals(false, skill.editable)
|
||||||
|
assertEquals("# Remote", skill.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `custom skills under non cache paths remain editable`() = runBlocking {
|
||||||
|
val dir = Files.createTempDirectory("kilo-skill-test")
|
||||||
|
val file = Files.createDirectories(dir.resolve("cache/kilo/skills/custom")).resolve("SKILL.md")
|
||||||
|
Files.writeString(file, "# Custom")
|
||||||
|
mock.skills = """[
|
||||||
|
{"name":"custom","description":"Custom","location":"$file","content":"# Custom"}
|
||||||
|
]""".trimIndent()
|
||||||
|
|
||||||
|
val skill = rpc().skills("/test project").single()
|
||||||
|
|
||||||
|
assertEquals(true, skill.editable)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `save skill supports configured markdown text and html files without reload`() = runBlocking {
|
||||||
|
val dir = Files.createTempDirectory("kilo-skill-test")
|
||||||
|
val file = dir.resolve("test.md")
|
||||||
|
Files.writeString(file, "old")
|
||||||
|
mock.skills = """[
|
||||||
|
{"name":"test","description":"Test","location":"$file","content":"old"}
|
||||||
|
]""".trimIndent()
|
||||||
|
val rpc = rpc()
|
||||||
|
|
||||||
|
assertTrue(rpc.saveSkill("/test project", file.toString(), "new content"))
|
||||||
|
assertEquals("new content", Files.readString(file))
|
||||||
|
assertEquals(0, mock.requestCount("/instance/reload"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `save skill writes content without reloading instance`() = runBlocking {
|
||||||
|
val dir = Files.createTempDirectory("kilo-skill-test")
|
||||||
|
val file = Files.createDirectories(dir.resolve("plan")).resolve("SKILL.md")
|
||||||
|
Files.writeString(file, "old")
|
||||||
|
mock.skills = """[
|
||||||
|
{"name":"plan","description":"Plan work","location":"$file","content":"old"}
|
||||||
|
]""".trimIndent()
|
||||||
|
val rpc = rpc()
|
||||||
|
|
||||||
|
assertTrue(rpc.saveSkill("/test project", file.toString(), "new content"))
|
||||||
|
|
||||||
|
assertEquals("new content", Files.readString(file))
|
||||||
|
assertEquals(0, mock.requestCount("/instance/reload"))
|
||||||
|
assertFalse(rpc.saveSkill("/test project", "builtin", "nope"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `save skills validates known paths once for multiple edits`() = runBlocking {
|
||||||
|
val dir = Files.createTempDirectory("kilo-skill-test")
|
||||||
|
val plan = Files.createDirectories(dir.resolve("plan")).resolve("SKILL.md")
|
||||||
|
val review = Files.createDirectories(dir.resolve("review")).resolve("SKILL.md")
|
||||||
|
Files.writeString(plan, "old plan")
|
||||||
|
Files.writeString(review, "old review")
|
||||||
|
mock.skills = """[
|
||||||
|
{"name":"plan","description":"Plan work","location":"$plan","content":"old plan"},
|
||||||
|
{"name":"review","description":"Review work","location":"$review","content":"old review"}
|
||||||
|
]""".trimIndent()
|
||||||
|
val rpc = rpc()
|
||||||
|
mock.resetCounts()
|
||||||
|
|
||||||
|
assertTrue(rpc.saveSkills("/test project", mapOf(plan.toString() to "new plan", review.toString() to "new review")))
|
||||||
|
|
||||||
|
assertEquals("new plan", Files.readString(plan))
|
||||||
|
assertEquals("new review", Files.readString(review))
|
||||||
|
assertEquals(1, mock.requestCount("/skill"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `save skill rejects unknown absolute skill files`() = runBlocking {
|
||||||
|
val dir = Files.createTempDirectory("kilo-skill-test")
|
||||||
|
val known = Files.createDirectories(dir.resolve("known")).resolve("SKILL.md")
|
||||||
|
val other = Files.createDirectories(dir.resolve("other")).resolve("SKILL.md")
|
||||||
|
Files.writeString(known, "known")
|
||||||
|
Files.writeString(other, "old")
|
||||||
|
mock.skills = """[
|
||||||
|
{"name":"known","description":"Known","location":"$known","content":"known"}
|
||||||
|
]""".trimIndent()
|
||||||
|
|
||||||
|
assertFalse(rpc().saveSkill("/test project", other.toString(), "new content"))
|
||||||
|
|
||||||
|
assertEquals("old", Files.readString(other))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `reload skills is blocked by pending permissions`() = runBlocking {
|
||||||
|
mock.pendingPermissions = """[
|
||||||
|
{"id":"per_test","sessionID":"ses_test","permission":"bash","patterns":["*"],"metadata":{}}
|
||||||
|
]""".trimIndent()
|
||||||
|
val rpc = rpc()
|
||||||
|
|
||||||
|
assertFalse(rpc.reloadSkills("/test project"))
|
||||||
|
|
||||||
|
assertEquals(1, mock.requestCount("/permission"))
|
||||||
|
assertEquals(0, mock.requestCount("/instance/reload"))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `mcp config writes global and workspace patches`() = runBlocking {
|
fun `mcp config writes global and workspace patches`() = runBlocking {
|
||||||
mock.config = """{"mcp":{"global":{"type":"local","command":["node","g.js"]}}}"""
|
mock.config = """{"mcp":{"global":{"type":"local","command":["node","g.js"]}}}"""
|
||||||
|
|||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
package ai.kilocode.backend.rpc
|
||||||
|
|
||||||
|
import ai.kilocode.backend.app.KiloAppState
|
||||||
|
import ai.kilocode.backend.app.KiloBackendAppService
|
||||||
|
import ai.kilocode.backend.testing.FakeCliServer
|
||||||
|
import ai.kilocode.backend.testing.MockCliServer
|
||||||
|
import ai.kilocode.backend.testing.TestLog
|
||||||
|
import ai.kilocode.rpc.dto.WorkspaceFileDto
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import java.nio.file.Files
|
||||||
|
import kotlin.test.AfterTest
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertIs
|
||||||
|
import kotlin.test.assertNotNull
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class KiloWorkspaceRpcApiImplTest {
|
||||||
|
private val mock = MockCliServer()
|
||||||
|
private val log = TestLog()
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
private val apps = mutableListOf<KiloBackendAppService>()
|
||||||
|
|
||||||
|
@AfterTest
|
||||||
|
fun tearDown() = runBlocking {
|
||||||
|
apps.forEach { it.dispose() }
|
||||||
|
apps.clear()
|
||||||
|
scope.cancel()
|
||||||
|
mock.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `searches files and directories through core`() = runBlocking {
|
||||||
|
mock.findFiles = """["src/Main.kt",".kilo/worktrees/hidden.kt"]"""
|
||||||
|
mock.findDirectories = """["src/","docs/"]"""
|
||||||
|
val dir = Files.createTempDirectory("kilo-search")
|
||||||
|
try {
|
||||||
|
val app = app()
|
||||||
|
|
||||||
|
val result = KiloWorkspaceRpcApiImpl(app).searchFiles(dir.toString(), "src", 3)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
listOf(
|
||||||
|
WorkspaceFileDto("src", "src", directory = true),
|
||||||
|
WorkspaceFileDto("docs", "docs", directory = true),
|
||||||
|
WorkspaceFileDto("src/Main.kt", "Main.kt"),
|
||||||
|
),
|
||||||
|
result.files,
|
||||||
|
)
|
||||||
|
assertEquals(2, mock.requestCount("/find/file"))
|
||||||
|
assertTrue(mock.findFilePaths.any { it.contains("type=file") && it.contains("query=src") })
|
||||||
|
assertTrue(mock.findFilePaths.any { it.contains("type=directory") && it.contains("query=src") })
|
||||||
|
} finally {
|
||||||
|
delete(dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun app(): KiloBackendAppService {
|
||||||
|
val app = KiloBackendAppService.create(scope, FakeCliServer(mock), log).also { apps.add(it) }
|
||||||
|
app.connect()
|
||||||
|
val state = assertNotNull(
|
||||||
|
withTimeoutOrNull(35_000) {
|
||||||
|
app.appState.first {
|
||||||
|
it is KiloAppState.Ready || it is KiloAppState.Error || it is KiloAppState.MigrationRequired
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"App startup timed out in ${app.appState.value}; logs=${log.messages}",
|
||||||
|
)
|
||||||
|
assertIs<KiloAppState.Ready>(state, "App startup failed; logs=${log.messages}")
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun delete(dir: java.nio.file.Path) {
|
||||||
|
Files.walk(dir).use { paths ->
|
||||||
|
paths.sorted(Comparator.reverseOrder()).forEach { Files.deleteIfExists(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
@@ -11,6 +11,7 @@ import java.net.ServerSocket
|
|||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
import java.net.SocketException
|
import java.net.SocketException
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -67,9 +68,11 @@ class MockCliServer : AutoCloseable {
|
|||||||
@Volatile var mcpStatus = 200
|
@Volatile var mcpStatus = 200
|
||||||
@Volatile var mcpActionStatus = 200
|
@Volatile var mcpActionStatus = 200
|
||||||
@Volatile var agentRemoveStatus = 200
|
@Volatile var agentRemoveStatus = 200
|
||||||
|
@Volatile var skillRemoveStatus = 200
|
||||||
@Volatile var agentBuilderStatus = 200
|
@Volatile var agentBuilderStatus = 200
|
||||||
@Volatile var lastMcpActionPath: String? = null
|
@Volatile var lastMcpActionPath: String? = null
|
||||||
@Volatile var lastAgentRemoveBody: String? = null
|
@Volatile var lastAgentRemoveBody: String? = null
|
||||||
|
@Volatile var lastSkillRemoveBody: String? = null
|
||||||
@Volatile var lastAgentBuilderPath: String? = null
|
@Volatile var lastAgentBuilderPath: String? = null
|
||||||
@Volatile var lastAgentBuilderBody: String? = null
|
@Volatile var lastAgentBuilderBody: String? = null
|
||||||
@Volatile var lastAgentBuilderMethod: String? = null
|
@Volatile var lastAgentBuilderMethod: String? = null
|
||||||
@@ -87,6 +90,12 @@ class MockCliServer : AutoCloseable {
|
|||||||
@Volatile var commandsStatus = 200
|
@Volatile var commandsStatus = 200
|
||||||
@Volatile var skillsStatus = 200
|
@Volatile var skillsStatus = 200
|
||||||
|
|
||||||
|
// File search responses
|
||||||
|
@Volatile var findFiles = "[]"
|
||||||
|
@Volatile var findDirectories = "[]"
|
||||||
|
@Volatile var findFileStatus = 200
|
||||||
|
val findFilePaths = CopyOnWriteArrayList<String>()
|
||||||
|
|
||||||
// Session REST responses
|
// Session REST responses
|
||||||
@Volatile var sessions = "[]"
|
@Volatile var sessions = "[]"
|
||||||
@Volatile var recentSessions = "[]"
|
@Volatile var recentSessions = "[]"
|
||||||
@@ -128,6 +137,8 @@ class MockCliServer : AutoCloseable {
|
|||||||
@Volatile var lastSessionRenamePath: String? = null
|
@Volatile var lastSessionRenamePath: String? = null
|
||||||
@Volatile var lastSessionRenameBody: String? = null
|
@Volatile var lastSessionRenameBody: String? = null
|
||||||
@Volatile var lastSessionRenameMethod: String? = null
|
@Volatile var lastSessionRenameMethod: String? = null
|
||||||
|
@Volatile var pendingPermissions = "[]"
|
||||||
|
@Volatile var pendingQuestions = "[]"
|
||||||
|
|
||||||
/** Configurable delay for all endpoint responses (ms). 0 = no delay. */
|
/** Configurable delay for all endpoint responses (ms). 0 = no delay. */
|
||||||
@Volatile var responseDelay: Long = 0
|
@Volatile var responseDelay: Long = 0
|
||||||
@@ -368,8 +379,18 @@ class MockCliServer : AutoCloseable {
|
|||||||
lastAgentRemoveBody = body
|
lastAgentRemoveBody = body
|
||||||
respond(output, agentRemoveStatus, if (agentRemoveStatus == 200) "true" else """{"error":"Agent not found"}""")
|
respond(output, agentRemoveStatus, if (agentRemoveStatus == 200) "true" else """{"error":"Agent not found"}""")
|
||||||
}
|
}
|
||||||
|
bare == "/kilocode/skill/remove" && method == "POST" -> {
|
||||||
|
lastSkillRemoveBody = body
|
||||||
|
respond(output, skillRemoveStatus, if (skillRemoveStatus == 200) "true" else """{"error":"Skill not found"}""")
|
||||||
|
}
|
||||||
|
bare == "/instance/reload" && method == "POST" -> respond(output, 200, "true")
|
||||||
bare == "/command" -> respond(output, commandsStatus, commands)
|
bare == "/command" -> respond(output, commandsStatus, commands)
|
||||||
bare == "/skill" -> respond(output, skillsStatus, skills)
|
bare == "/skill" -> respond(output, skillsStatus, skills)
|
||||||
|
bare == "/find/file" -> {
|
||||||
|
findFilePaths.add(path)
|
||||||
|
val body = if (path.contains("type=directory")) findDirectories else findFiles
|
||||||
|
respond(output, findFileStatus, body)
|
||||||
|
}
|
||||||
bare == "/mcp" -> respond(output, mcpStatus, mcp)
|
bare == "/mcp" -> respond(output, mcpStatus, mcp)
|
||||||
bare.matches(Regex("/mcp/[^/]+/(connect|disconnect)")) && method == "POST" -> {
|
bare.matches(Regex("/mcp/[^/]+/(connect|disconnect)")) && method == "POST" -> {
|
||||||
lastMcpActionPath = path
|
lastMcpActionPath = path
|
||||||
@@ -393,6 +414,8 @@ class MockCliServer : AutoCloseable {
|
|||||||
respond(output, cloudSessionImportStatus, cloudSessionImport)
|
respond(output, cloudSessionImportStatus, cloudSessionImport)
|
||||||
}
|
}
|
||||||
bare == "/session/status" -> respond(output, sessionStatusesStatus, sessionStatuses)
|
bare == "/session/status" -> respond(output, sessionStatusesStatus, sessionStatuses)
|
||||||
|
bare == "/permission" && method == "GET" -> respond(output, 200, pendingPermissions)
|
||||||
|
bare == "/question" && method == "GET" -> respond(output, 200, pendingQuestions)
|
||||||
bare == "/session" && method == "GET" -> respond(output, sessionsStatus, sessions)
|
bare == "/session" && method == "GET" -> respond(output, sessionsStatus, sessions)
|
||||||
bare == "/session" && method == "POST" -> respond(output, sessionCreateStatus, sessionCreate)
|
bare == "/session" && method == "POST" -> respond(output, sessionCreateStatus, sessionCreate)
|
||||||
bare.matches(Regex("/session/ses_[^/]+")) && method == "GET" ->
|
bare.matches(Regex("/session/ses_[^/]+")) && method == "GET" ->
|
||||||
|
|||||||
+9
@@ -21,4 +21,13 @@ object KiloNotifications {
|
|||||||
?: Notification(GROUP, title, content ?: "", NotificationType.ERROR)
|
?: Notification(GROUP, title, content ?: "", NotificationType.ERROR)
|
||||||
notification.notify(project)
|
notification.notify(project)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun info(title: String, content: String? = null) {
|
||||||
|
val project = ProjectManager.getInstance().openProjects.firstOrNull { !it.isDefault }
|
||||||
|
val notification = NotificationGroupManager.getInstance()
|
||||||
|
.getNotificationGroup(GROUP)
|
||||||
|
?.createNotification(title, content ?: "", NotificationType.INFORMATION)
|
||||||
|
?: Notification(GROUP, title, content ?: "", NotificationType.INFORMATION)
|
||||||
|
notification.notify(project)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-1
@@ -33,7 +33,9 @@ class KiloAgentBehaviorService internal constructor(
|
|||||||
|
|
||||||
suspend fun agents(directory: String): List<AgentDetailDto> = safe(emptyList()) { call { agents(directory) } }
|
suspend fun agents(directory: String): List<AgentDetailDto> = safe(emptyList()) { call { agents(directory) } }
|
||||||
|
|
||||||
suspend fun skills(directory: String): List<SkillDto> = safe(emptyList()) { call { skills(directory) } }
|
suspend fun loadSkills(directory: String): List<SkillDto> = call { skills(directory) }
|
||||||
|
|
||||||
|
suspend fun refreshSkills(directory: String, fallback: List<SkillDto>): List<SkillDto> = safe(fallback) { call { skills(directory) } }
|
||||||
|
|
||||||
suspend fun commands(directory: String): List<CommandDto> = safe(emptyList()) { call { commands(directory) } }
|
suspend fun commands(directory: String): List<CommandDto> = safe(emptyList()) { call { commands(directory) } }
|
||||||
|
|
||||||
@@ -52,6 +54,11 @@ class KiloAgentBehaviorService internal constructor(
|
|||||||
|
|
||||||
suspend fun removeSkill(directory: String, location: String): Boolean = safe(false) { call { removeSkill(directory, location) } }
|
suspend fun removeSkill(directory: String, location: String): Boolean = safe(false) { call { removeSkill(directory, location) } }
|
||||||
|
|
||||||
|
suspend fun reloadSkills(directory: String): Boolean = safe(false) { call { reloadSkills(directory) } }
|
||||||
|
|
||||||
|
suspend fun saveSkills(directory: String, edits: Map<String, String>): Boolean =
|
||||||
|
safe(false) { call { saveSkills(directory, edits) } }
|
||||||
|
|
||||||
suspend fun removeAgent(directory: String, name: String): Boolean = safe(false) { call { removeAgent(directory, name) } }
|
suspend fun removeAgent(directory: String, name: String): Boolean = safe(false) { call { removeAgent(directory, name) } }
|
||||||
|
|
||||||
suspend fun createAgent(directory: String, input: AgentCreateDto): Boolean = safe(false) { call { createAgent(directory, input) } }
|
suspend fun createAgent(directory: String, input: AgentCreateDto): Boolean = safe(false) { call { createAgent(directory, input) } }
|
||||||
|
|||||||
+10
@@ -139,6 +139,7 @@ class KiloWorkspaceService internal constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun searchFiles(directory: String, query: String, limit: Int = 50): FileSearchResultDto {
|
suspend fun searchFiles(directory: String, query: String, limit: Int = 50): FileSearchResultDto {
|
||||||
|
LOG.debug { "workspace file search directory=$directory query=$query limit=$limit" }
|
||||||
return try {
|
return try {
|
||||||
call { searchFiles(directory, query, limit) }
|
call { searchFiles(directory, query, limit) }
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
@@ -168,6 +169,15 @@ class KiloWorkspaceService internal constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun openFile(path: String, line: Int? = null, column: Int? = null): Boolean {
|
||||||
|
return try {
|
||||||
|
call { openFile(path, line, column) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LOG.warn("workspace file open failed for path=$path", e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun localConfigTarget(directory: String): ConfigTargetDto? {
|
suspend fun localConfigTarget(directory: String): ConfigTargetDto? {
|
||||||
return try {
|
return try {
|
||||||
val target = call { this.localConfigTarget(directory) }
|
val target = call { this.localConfigTarget(directory) }
|
||||||
|
|||||||
+1
-2
@@ -10,7 +10,6 @@ import com.intellij.openapi.actionSystem.ActionManager
|
|||||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||||
import com.intellij.openapi.actionSystem.DataSink
|
import com.intellij.openapi.actionSystem.DataSink
|
||||||
import com.intellij.openapi.actionSystem.IdeActions
|
import com.intellij.openapi.actionSystem.IdeActions
|
||||||
import com.intellij.openapi.actionSystem.PlatformCoreDataKeys
|
|
||||||
import com.intellij.openapi.command.undo.UndoManager
|
import com.intellij.openapi.command.undo.UndoManager
|
||||||
import com.intellij.openapi.editor.Editor
|
import com.intellij.openapi.editor.Editor
|
||||||
import com.intellij.openapi.editor.ex.EditorEx
|
import com.intellij.openapi.editor.ex.EditorEx
|
||||||
@@ -74,11 +73,11 @@ internal open class SessionEditorTextField(
|
|||||||
super.uiDataSnapshot(sink)
|
super.uiDataSnapshot(sink)
|
||||||
selection?.provideCopy(sink) { text }
|
selection?.provideCopy(sink) { text }
|
||||||
ctx?.let { sink.set(PromptDataKeys.SEND, it) }
|
ctx?.let { sink.set(PromptDataKeys.SEND, it) }
|
||||||
file()?.let { sink.set(PlatformCoreDataKeys.FILE_EDITOR, it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun install(editor: Editor) {
|
private fun install(editor: Editor) {
|
||||||
(editor as? EditorEx)?.setEmbeddedIntoDialogWrapper(true)
|
(editor as? EditorEx)?.setEmbeddedIntoDialogWrapper(true)
|
||||||
|
editor.putUserData(EditorTextField.SUPPLEMENTARY_KEY, true)
|
||||||
// EditorImpl lazily creates EditorFloatingToolbar with the same first-show hook.
|
// EditorImpl lazily creates EditorFloatingToolbar with the same first-show hook.
|
||||||
// Settings providers run later, so this callback runs immediately after toolbar creation.
|
// Settings providers run later, so this callback runs immediately after toolbar creation.
|
||||||
UiNotifyConnector.doWhenFirstShown(editor.component) { hide(editor.component) }
|
UiNotifyConnector.doWhenFirstShown(editor.component) { hide(editor.component) }
|
||||||
|
|||||||
+1
-1
@@ -73,8 +73,8 @@ class KiloPromptCompletionProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun prewarm() {
|
fun prewarm() {
|
||||||
if (cache.containsKey("")) return
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
if (cache.containsKey("")) return@launch
|
||||||
val result = service.searchFiles(workspace.directory, "", 50)
|
val result = service.searchFiles(workspace.directory, "", 50)
|
||||||
if (result.files.isNotEmpty() || result.git) cache.putIfAbsent("", result)
|
if (result.files.isNotEmpty() || result.git) cache.putIfAbsent("", result)
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -2,6 +2,7 @@ package ai.kilocode.client.settings
|
|||||||
|
|
||||||
import ai.kilocode.client.plugin.KiloBundle
|
import ai.kilocode.client.plugin.KiloBundle
|
||||||
import ai.kilocode.client.settings.agents.AgentBehaviorConfigurable
|
import ai.kilocode.client.settings.agents.AgentBehaviorConfigurable
|
||||||
|
import ai.kilocode.client.settings.context.ContextConfigurable
|
||||||
import ai.kilocode.client.settings.models.ModelsConfigurable
|
import ai.kilocode.client.settings.models.ModelsConfigurable
|
||||||
import ai.kilocode.client.settings.providers.ProvidersConfigurable
|
import ai.kilocode.client.settings.providers.ProvidersConfigurable
|
||||||
import ai.kilocode.client.settings.profile.UserProfileConfigurable
|
import ai.kilocode.client.settings.profile.UserProfileConfigurable
|
||||||
@@ -73,6 +74,14 @@ class KiloSettingsConfigurable : SearchableConfigurable {
|
|||||||
behavior.border = JBUI.Borders.emptyBottom(UiStyle.Gap.sm())
|
behavior.border = JBUI.Borders.emptyBottom(UiStyle.Gap.sm())
|
||||||
panel.next(behavior)
|
panel.next(behavior)
|
||||||
|
|
||||||
|
val context = ActionLink(KiloBundle.message("settings.context.displayName")) { e ->
|
||||||
|
val src = e.source as? JComponent ?: return@ActionLink
|
||||||
|
val settings = Settings.KEY.getData(DataManager.getInstance().getDataContext(src)) ?: return@ActionLink
|
||||||
|
open(settings, ContextConfigurable.ID)
|
||||||
|
}
|
||||||
|
context.border = JBUI.Borders.emptyBottom(UiStyle.Gap.sm())
|
||||||
|
panel.next(context)
|
||||||
|
|
||||||
return panel
|
return panel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
@@ -25,6 +25,7 @@ class AgentBehaviorConfigurable : SearchableConfigurable {
|
|||||||
listOf(
|
listOf(
|
||||||
KiloBundle.message("settings.agentBehavior.agents.displayName") to AgentsConfigurable.ID,
|
KiloBundle.message("settings.agentBehavior.agents.displayName") to AgentsConfigurable.ID,
|
||||||
KiloBundle.message("settings.agentBehavior.mcp.displayName") to McpConfigurable.ID,
|
KiloBundle.message("settings.agentBehavior.mcp.displayName") to McpConfigurable.ID,
|
||||||
|
KiloBundle.message("settings.agentBehavior.skills.displayName") to SkillsConfigurable.ID,
|
||||||
).forEach { (label, id) ->
|
).forEach { (label, id) ->
|
||||||
panel.next(ActionLink(label) { e ->
|
panel.next(ActionLink(label) { e ->
|
||||||
val src = e.source as? JComponent ?: return@ActionLink
|
val src = e.source as? JComponent ?: return@ActionLink
|
||||||
|
|||||||
+586
@@ -0,0 +1,586 @@
|
|||||||
|
package ai.kilocode.client.settings.agents
|
||||||
|
|
||||||
|
import ai.kilocode.client.app.KiloAgentBehaviorService
|
||||||
|
import ai.kilocode.client.app.KiloAppService
|
||||||
|
import ai.kilocode.client.app.KiloWorkspaceService
|
||||||
|
import ai.kilocode.client.KiloNotifications
|
||||||
|
import ai.kilocode.client.plugin.KiloBundle
|
||||||
|
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||||
|
import ai.kilocode.client.settings.base.SettingsBadge
|
||||||
|
import ai.kilocode.client.settings.base.SettingsDraftPage
|
||||||
|
import ai.kilocode.client.settings.base.SettingsDraftState
|
||||||
|
import ai.kilocode.client.settings.base.SettingsListCell
|
||||||
|
import ai.kilocode.client.settings.base.SettingsListConfig
|
||||||
|
import ai.kilocode.client.settings.base.SettingsListItem
|
||||||
|
import ai.kilocode.client.settings.base.SettingsListPanel
|
||||||
|
import ai.kilocode.client.settings.base.SettingsListSelection
|
||||||
|
import ai.kilocode.client.settings.base.SettingsListView
|
||||||
|
import ai.kilocode.client.settings.base.SettingsMessageException
|
||||||
|
import ai.kilocode.client.ui.UiStyle
|
||||||
|
import ai.kilocode.client.ui.layout.Stack
|
||||||
|
import ai.kilocode.log.KiloLog
|
||||||
|
import ai.kilocode.rpc.dto.ConfigPatchDto
|
||||||
|
import ai.kilocode.rpc.dto.SkillsConfigDto
|
||||||
|
import ai.kilocode.rpc.dto.SkillsPatchDto
|
||||||
|
import ai.kilocode.rpc.dto.SkillDto
|
||||||
|
import com.intellij.CommonBundle
|
||||||
|
import com.intellij.icons.AllIcons
|
||||||
|
import com.intellij.openapi.actionSystem.ActionManager
|
||||||
|
import com.intellij.openapi.actionSystem.ActionPlaces
|
||||||
|
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||||
|
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||||
|
import com.intellij.openapi.actionSystem.DefaultActionGroup
|
||||||
|
import com.intellij.openapi.application.EDT
|
||||||
|
import com.intellij.openapi.application.ModalityState
|
||||||
|
import com.intellij.openapi.application.asContextElement
|
||||||
|
import com.intellij.openapi.components.service
|
||||||
|
import com.intellij.openapi.editor.EditorFactory
|
||||||
|
import com.intellij.openapi.editor.event.DocumentEvent
|
||||||
|
import com.intellij.openapi.editor.event.DocumentListener
|
||||||
|
import com.intellij.openapi.fileChooser.FileChooser
|
||||||
|
import com.intellij.openapi.fileChooser.FileChooserDescriptor
|
||||||
|
import com.intellij.openapi.fileTypes.FileType
|
||||||
|
import com.intellij.openapi.fileTypes.FileTypeManager
|
||||||
|
import com.intellij.openapi.fileTypes.PlainTextFileType
|
||||||
|
import com.intellij.openapi.fileTypes.UnknownFileType
|
||||||
|
import com.intellij.openapi.project.DumbAwareAction
|
||||||
|
import com.intellij.openapi.project.ProjectManager
|
||||||
|
import com.intellij.openapi.ui.DialogWrapper
|
||||||
|
import com.intellij.openapi.ui.Messages
|
||||||
|
import com.intellij.openapi.vfs.VirtualFile
|
||||||
|
import com.intellij.ui.EditorTextField
|
||||||
|
import com.intellij.ui.TitledSeparator
|
||||||
|
import com.intellij.ui.components.JBScrollPane
|
||||||
|
import com.intellij.ui.components.JBTextField
|
||||||
|
import com.intellij.util.ui.JBUI
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import javax.swing.JButton
|
||||||
|
import javax.swing.JComponent
|
||||||
|
import javax.swing.JPanel
|
||||||
|
import javax.swing.ScrollPaneConstants
|
||||||
|
import javax.swing.ListSelectionModel
|
||||||
|
|
||||||
|
private val edt = Dispatchers.EDT + ModalityState.any().asContextElement()
|
||||||
|
|
||||||
|
class SkillsConfigurable : AgentBehaviorConfigurableBase<JComponent>() {
|
||||||
|
override fun getId(): String = ID
|
||||||
|
override fun getDisplayName(): String = KiloBundle.message("settings.agentBehavior.skills.displayName")
|
||||||
|
override fun create(cs: CoroutineScope, dir: String): JComponent = SkillsSettingsUi(cs, dir)
|
||||||
|
override fun update(ui: JComponent, dir: String) {
|
||||||
|
(ui as? SkillsSettingsUi)?.setDirectory(dir)
|
||||||
|
}
|
||||||
|
override fun scrollReadyShell() = false
|
||||||
|
|
||||||
|
companion object { const val ID = "ai.kilocode.jetbrains.settings.agentBehavior.skills" }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SkillsSettingsUi(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
dir: String,
|
||||||
|
private val choose: (JComponent) -> String? = ::chooseSkillPath,
|
||||||
|
private val input: (String, String) -> String? = ::inputSkillUrl,
|
||||||
|
private val edit: (SkillDto, Boolean) -> SkillEditDialogHandle = ::SkillEditDialog,
|
||||||
|
) : SettingsListPanel(scope, SettingsListConfig.Equal.copy(tooltip = false)), SettingsDraftPage {
|
||||||
|
private val cs = scope
|
||||||
|
private var dir = dir
|
||||||
|
private var skills = emptyMap<String, SkillDto>()
|
||||||
|
private val app get() = service<KiloAppService>()
|
||||||
|
private val state = SettingsDraftState(skillsDraft(app.state.value.config?.skills ?: SkillsConfigDto()), ::saved)
|
||||||
|
private var draft: SkillsDraft
|
||||||
|
get() = state.draft
|
||||||
|
set(value) {
|
||||||
|
state.draft = value
|
||||||
|
}
|
||||||
|
internal val sources = SkillSourcesView(this, choose, input)
|
||||||
|
|
||||||
|
init {
|
||||||
|
start()
|
||||||
|
setCenter(skillScroll())
|
||||||
|
content.add(sources, BorderLayout.SOUTH)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDirectory(value: String) {
|
||||||
|
if (value == dir) return
|
||||||
|
dir = value
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun fetch(): List<SettingsListItem> {
|
||||||
|
val items = withTimeoutOrNull(SKILL_LOAD_TIMEOUT_MS) {
|
||||||
|
service<KiloAgentBehaviorService>().loadSkills(dir)
|
||||||
|
} ?: throw SettingsMessageException(KiloBundle.message("settings.agentBehavior.skills.load.timeout"))
|
||||||
|
withContext(edt) {
|
||||||
|
val dirty = state.modified()
|
||||||
|
val edit = draft
|
||||||
|
state.accept(skillsDraft(config()))
|
||||||
|
if (dirty) draft = state.draft.copy(edited = edit.edited, deleted = edit.deleted)
|
||||||
|
skills = items.associateBy { key(it) }
|
||||||
|
sources.refresh(draft.sources)
|
||||||
|
}
|
||||||
|
LOG.info("skills settings fetch dir=$dir total=${items.size}")
|
||||||
|
return rows(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun afterApply() {
|
||||||
|
sources.refresh(draft.sources)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCell(key: String, cellId: String) {
|
||||||
|
val skill = skills[key] ?: return
|
||||||
|
when (cellId) {
|
||||||
|
OPEN_CELL -> open(skill)
|
||||||
|
EDIT_CELL -> edit(skill)
|
||||||
|
DELETE_CELL -> remove(skill)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchPlaceholder() = KiloBundle.message("settings.agentBehavior.skills.search")
|
||||||
|
|
||||||
|
override fun emptyText() = KiloBundle.message("settings.agentBehavior.skills.empty")
|
||||||
|
|
||||||
|
internal fun updateSources(paths: List<String>, urls: List<String>) {
|
||||||
|
state.update { copy(sources = SkillsConfigDto(paths = paths, urls = urls)) }
|
||||||
|
sources.refresh(draft.sources)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun modified(): Boolean = state.modified()
|
||||||
|
|
||||||
|
override fun resetDraft() {
|
||||||
|
state.reset()
|
||||||
|
sources.refresh(draft.sources)
|
||||||
|
view.update(rows())
|
||||||
|
clearProgress()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun applyDraft() {
|
||||||
|
val token = state.start() ?: return
|
||||||
|
val fallback = skillFallback(token.target)
|
||||||
|
if (!launch("apply") { id ->
|
||||||
|
val target = token.target
|
||||||
|
var failed: String? = null
|
||||||
|
val behavior = service<KiloAgentBehaviorService>()
|
||||||
|
LOG.info("skills settings apply start dir=$dir edited=${target.edited.size} deleted=${target.deleted.size} paths=${target.sources.paths.size} urls=${target.sources.urls.size}")
|
||||||
|
if (target.edited.isNotEmpty() && !behavior.saveSkills(dir, target.edited)) {
|
||||||
|
failed = KiloBundle.message("settings.agentBehavior.save.failed")
|
||||||
|
}
|
||||||
|
if (failed == null) {
|
||||||
|
for (location in target.deleted) {
|
||||||
|
if (!behavior.removeSkill(dir, location)) {
|
||||||
|
failed = KiloBundle.message("settings.agentBehavior.skills.delete.failed")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (failed == null && target.sources != token.previous.sources) {
|
||||||
|
val patch = ConfigPatchDto(skills = SkillsPatchDto(paths = target.sources.paths, urls = target.sources.urls))
|
||||||
|
if (app.updateConfig(patch) == null) failed = KiloBundle.message("settings.agentBehavior.save.failed")
|
||||||
|
}
|
||||||
|
val reloaded = if (failed == null) behavior.reloadSkills(dir) else true
|
||||||
|
val items = behavior.refreshSkills(dir, fallback)
|
||||||
|
withContext(edt) {
|
||||||
|
if (!active(id)) {
|
||||||
|
if (failed == null) KiloNotifications.info(KiloBundle.message("settings.agentBehavior.skills.saved.notification"))
|
||||||
|
else KiloNotifications.error(failed)
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
if (failed == null) {
|
||||||
|
skills = items.associateBy { key(it) }
|
||||||
|
val next = skillsDraft(config())
|
||||||
|
state.complete(token, next)
|
||||||
|
sources.refresh(draft.sources)
|
||||||
|
view.update(rows(items))
|
||||||
|
if (reloaded) clearProgress() else showProgress(KiloBundle.message("settings.agentBehavior.skills.reload.blocked"))
|
||||||
|
LOG.info("skills settings apply succeeded dir=$dir")
|
||||||
|
} else {
|
||||||
|
state.fail(token, failed)
|
||||||
|
sources.refresh(draft.sources)
|
||||||
|
view.update(rows(items))
|
||||||
|
showError(failed)
|
||||||
|
LOG.warn("skills settings apply failed dir=$dir message=$failed")
|
||||||
|
}
|
||||||
|
setBusy(false)
|
||||||
|
}
|
||||||
|
}) return
|
||||||
|
showProgress(KiloBundle.message("settings.agentBehavior.saving"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun skillScroll() = JBScrollPane(view).apply {
|
||||||
|
border = null
|
||||||
|
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||||
|
verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rows(items: List<SkillDto> = skills.values.toList()): List<SettingsListItem> = items.mapNotNull { skill ->
|
||||||
|
if (skill.location in draft.deleted) return@mapNotNull null
|
||||||
|
item(skill)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun skillFallback(target: SkillsDraft): List<SkillDto> = skills.values.mapNotNull { skill ->
|
||||||
|
if (skill.location in target.deleted) return@mapNotNull null
|
||||||
|
target.edited[skill.location]?.let { skill.copy(content = it) } ?: skill
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun item(skill: SkillDto) = object : SettingsListItem {
|
||||||
|
override val key = key(skill)
|
||||||
|
override val title = skill.name
|
||||||
|
override val note = skill.location.takeUnless { builtin(it) }
|
||||||
|
override val description = skill.description
|
||||||
|
override val doubleClick = EDIT_CELL
|
||||||
|
override val badges = listOf(
|
||||||
|
SettingsBadge(KiloBundle.message("settings.agentBehavior.badge.builtin"), UiStyle.Badge.Secondary),
|
||||||
|
).takeIf { builtin(skill.location) } ?: emptyList()
|
||||||
|
override val cells = listOfNotNull(
|
||||||
|
SettingsListCell(
|
||||||
|
OPEN_CELL,
|
||||||
|
KiloBundle.message("settings.agentBehavior.skills.openInEditor"),
|
||||||
|
primary = true,
|
||||||
|
).takeIf { skill.editable },
|
||||||
|
SettingsListCell(
|
||||||
|
EDIT_CELL,
|
||||||
|
KiloBundle.message(if (skill.editable) "settings.agentBehavior.edit" else "common.open"),
|
||||||
|
primary = !skill.editable,
|
||||||
|
),
|
||||||
|
SettingsListCell(
|
||||||
|
DELETE_CELL,
|
||||||
|
KiloBundle.message("common.delete"),
|
||||||
|
icon = AllIcons.Actions.GC,
|
||||||
|
iconOnly = true,
|
||||||
|
).takeIf { skill.editable },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun edit(skill: SkillDto) {
|
||||||
|
val current = skill.copy(content = content(skill))
|
||||||
|
val dialog = edit(current, skill.editable)
|
||||||
|
if (!skill.editable) {
|
||||||
|
dialog.showAndGet()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!dialog.showAndGet()) return
|
||||||
|
state.update { copy(edited = edited + (skill.location to dialog.content())) }
|
||||||
|
view.update(rows(), SettingsListSelection.Key(key(skill)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun open(skill: SkillDto) {
|
||||||
|
if (!skill.editable) return
|
||||||
|
showProgress(KiloBundle.message("settings.agentBehavior.skills.openInEditor.pending"))
|
||||||
|
cs.launch {
|
||||||
|
val opened = service<KiloWorkspaceService>().openFile(skill.location)
|
||||||
|
if (opened) return@launch
|
||||||
|
withContext(edt) { KiloNotifications.error(KiloBundle.message("settings.agentBehavior.skills.openInEditor.failed")) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun remove(skill: SkillDto) {
|
||||||
|
val result = Messages.showYesNoDialog(
|
||||||
|
KiloBundle.message("settings.agentBehavior.skills.delete.message", skill.name),
|
||||||
|
KiloBundle.message("settings.agentBehavior.skills.delete.title"),
|
||||||
|
KiloBundle.message("common.delete"),
|
||||||
|
Messages.getCancelButton(),
|
||||||
|
Messages.getQuestionIcon(),
|
||||||
|
)
|
||||||
|
if (result != Messages.YES) return
|
||||||
|
state.update { copy(deleted = deleted + skill.location, edited = edited - skill.location) }
|
||||||
|
view.update(rows(), selectionIndex())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun content(skill: SkillDto) = draft.edited[skill.location] ?: skill.content
|
||||||
|
|
||||||
|
private fun config() = app.state.value.config?.skills ?: SkillsConfigDto()
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val EDIT_CELL = "edit"
|
||||||
|
const val OPEN_CELL = "open"
|
||||||
|
const val DELETE_CELL = "delete"
|
||||||
|
const val BUILTIN = "builtin"
|
||||||
|
const val LEGACY_BUILTIN = "<built-in>"
|
||||||
|
val LOG = KiloLog.create(SkillsSettingsUi::class.java)
|
||||||
|
|
||||||
|
fun key(skill: SkillDto) = skill.location.ifBlank { skill.name }
|
||||||
|
fun builtin(location: String) = location == BUILTIN || location == LEGACY_BUILTIN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface SkillEditDialogHandle {
|
||||||
|
fun showAndGet(): Boolean
|
||||||
|
fun content(): String
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class SkillsDraft(
|
||||||
|
val sources: SkillsConfigDto,
|
||||||
|
val edited: Map<String, String> = emptyMap(),
|
||||||
|
val deleted: Set<String> = emptySet(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun skillsDraft(sources: SkillsConfigDto) = SkillsDraft(sources)
|
||||||
|
|
||||||
|
private fun saved(base: SkillsDraft, draft: SkillsDraft): Boolean = base == draft
|
||||||
|
|
||||||
|
internal class SkillEditDialog(private val skill: SkillDto, private val savable: Boolean) : DialogWrapper(true), SkillEditDialogHandle {
|
||||||
|
private val base = initial()
|
||||||
|
private val editor = SkillEditor(base, skill.location, savable)
|
||||||
|
|
||||||
|
init {
|
||||||
|
title = skill.name
|
||||||
|
setOKButtonText(CommonBundle.getOkButtonText())
|
||||||
|
setCancelButtonText(CommonBundle.getCloseButtonText())
|
||||||
|
init()
|
||||||
|
isOKActionEnabled = false
|
||||||
|
editor.document.addDocumentListener(object : DocumentListener {
|
||||||
|
override fun documentChanged(event: DocumentEvent) {
|
||||||
|
isOKActionEnabled = savable && editor.text != base
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createCenterPanel(): JComponent = JBScrollPane(editor).apply {
|
||||||
|
viewportBorder = editorPad()
|
||||||
|
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||||
|
verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
|
||||||
|
preferredSize = JBUI.size(720, 520)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createActions() = if (savable) arrayOf(okAction, cancelAction) else arrayOf(cancelAction)
|
||||||
|
|
||||||
|
override fun content() = editor.text
|
||||||
|
|
||||||
|
private fun initial() = skill.content?.takeIf { it.isNotBlank() }
|
||||||
|
?: skill.description?.takeIf { it.isNotBlank() }
|
||||||
|
?: KiloBundle.message("settings.agentBehavior.skills.content.empty")
|
||||||
|
|
||||||
|
private class SkillEditor(value: String, location: String, editable: Boolean) : EditorTextField(
|
||||||
|
EditorFactory.getInstance().createDocument(value),
|
||||||
|
ProjectManager.getInstance().defaultProject,
|
||||||
|
skillFileType(location, value),
|
||||||
|
false,
|
||||||
|
!editable,
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
border = JBUI.Borders.empty()
|
||||||
|
setOneLineMode(false)
|
||||||
|
addSettingsProvider { ed ->
|
||||||
|
ed.setBorder(JBUI.Borders.empty())
|
||||||
|
ed.scrollPane.border = JBUI.Borders.empty()
|
||||||
|
ed.scrollPane.viewportBorder = JBUI.Borders.empty()
|
||||||
|
ed.settings.isUseSoftWraps = true
|
||||||
|
ed.settings.isPaintSoftWraps = false
|
||||||
|
ed.settings.isAdditionalPageAtBottom = false
|
||||||
|
ed.scrollPane.horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||||
|
ed.scrollPane.verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SkillSourcesView(
|
||||||
|
private val parent: SkillsSettingsUi,
|
||||||
|
private val choose: (JComponent) -> String?,
|
||||||
|
private val input: (String, String) -> String?,
|
||||||
|
) : Stack(ai.kilocode.client.ui.layout.StackAxis.VERTICAL, UiStyle.Gap.sm()) {
|
||||||
|
private val view = SettingsListView(
|
||||||
|
KiloBundle.message("settings.agentBehavior.skills.sources.empty"),
|
||||||
|
SettingsListConfig.Preferred.copy(description = false, selection = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION),
|
||||||
|
) { key, id ->
|
||||||
|
if (id == EDIT_CELL) edit(key)
|
||||||
|
}
|
||||||
|
private var cfg = SkillsConfigDto()
|
||||||
|
|
||||||
|
internal fun sourceList() = view.list
|
||||||
|
|
||||||
|
init {
|
||||||
|
border = JBUI.Borders.empty(UiStyle.Gap.pad(), 0, 0, 0)
|
||||||
|
next(TitledSeparator(KiloBundle.message("settings.agentBehavior.skills.sources.title")))
|
||||||
|
next(toolbar())
|
||||||
|
next(JBScrollPane(view).apply {
|
||||||
|
border = null
|
||||||
|
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||||
|
preferredSize = JBUI.size(0, JBUI.scale(160))
|
||||||
|
maximumSize = JBUI.size(Int.MAX_VALUE, JBUI.scale(160))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh(config: SkillsConfigDto) {
|
||||||
|
cfg = config
|
||||||
|
view.update(rows(config))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toolbar(): JComponent {
|
||||||
|
val add = DefaultActionGroup(KiloBundle.message("settings.agentBehavior.skills.sources.add"), true).apply {
|
||||||
|
templatePresentation.icon = AllIcons.General.Add
|
||||||
|
add(AddPathAction())
|
||||||
|
add(AddUrlAction())
|
||||||
|
}
|
||||||
|
val group = DefaultActionGroup(add, RemoveAction())
|
||||||
|
val toolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.TOOLBAR, group, true)
|
||||||
|
toolbar.targetComponent = this
|
||||||
|
toolbar.updateActionsImmediately()
|
||||||
|
return toolbar.component
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun addPath() {
|
||||||
|
val path = choose(parent)?.trim()?.takeIf { it.isNotBlank() } ?: return
|
||||||
|
if (path in cfg.paths) return
|
||||||
|
parent.updateSources(cfg.paths + path, cfg.urls)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun addUrl() {
|
||||||
|
val url = input(
|
||||||
|
KiloBundle.message("settings.agentBehavior.skills.sources.addUrl.title"),
|
||||||
|
KiloBundle.message("settings.agentBehavior.skills.sources.addUrl.prompt"),
|
||||||
|
)?.trim()?.takeIf { it.isNotBlank() } ?: return
|
||||||
|
if (url in cfg.urls) return
|
||||||
|
parent.updateSources(cfg.paths, cfg.urls + url)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rows(config: SkillsConfigDto): List<SettingsListItem> {
|
||||||
|
val paths = config.paths.map { source(PATH_PREFIX, it) }
|
||||||
|
val urls = config.urls.map { source(URL_PREFIX, it) }
|
||||||
|
return paths + urls
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun source(prefix: String, value: String) = object : SettingsListItem {
|
||||||
|
override val key = prefix + value
|
||||||
|
override val title = value
|
||||||
|
override val doubleClick = EDIT_CELL
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun removeSelected() {
|
||||||
|
val keys = view.selectedItems().map { it.key }.toSet()
|
||||||
|
if (keys.isEmpty()) return
|
||||||
|
val paths = cfg.paths.filterNot { PATH_PREFIX + it in keys }
|
||||||
|
val urls = cfg.urls.filterNot { URL_PREFIX + it in keys }
|
||||||
|
parent.updateSources(paths, urls)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun edit(key: String) {
|
||||||
|
val path = key.startsWith(PATH_PREFIX)
|
||||||
|
val old = key.removePrefix(if (path) PATH_PREFIX else URL_PREFIX)
|
||||||
|
val dialog = SkillSourceDialog(old, path, choose)
|
||||||
|
if (!dialog.showAndGet()) return
|
||||||
|
val next = dialog.value().trim().takeIf { it.isNotBlank() } ?: return
|
||||||
|
if (path) {
|
||||||
|
parent.updateSources(cfg.paths.map { if (it == old) next else it }.distinct(), cfg.urls)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parent.updateSources(cfg.paths, cfg.urls.map { if (it == old) next else it }.distinct())
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class AddPathAction : DumbAwareAction(
|
||||||
|
KiloBundle.message("settings.agentBehavior.skills.sources.addPath"),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
) {
|
||||||
|
override fun getActionUpdateThread() = ActionUpdateThread.EDT
|
||||||
|
override fun actionPerformed(e: AnActionEvent) = addPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class AddUrlAction : DumbAwareAction(
|
||||||
|
KiloBundle.message("settings.agentBehavior.skills.sources.addUrl"),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
) {
|
||||||
|
override fun getActionUpdateThread() = ActionUpdateThread.EDT
|
||||||
|
override fun actionPerformed(e: AnActionEvent) = addUrl()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class RemoveAction : DumbAwareAction(
|
||||||
|
KiloBundle.message("common.delete"),
|
||||||
|
null,
|
||||||
|
AllIcons.General.Remove,
|
||||||
|
) {
|
||||||
|
override fun getActionUpdateThread() = ActionUpdateThread.EDT
|
||||||
|
override fun update(e: AnActionEvent) {
|
||||||
|
e.presentation.isEnabled = view.selectedItems().isNotEmpty()
|
||||||
|
}
|
||||||
|
override fun actionPerformed(e: AnActionEvent) = removeSelected()
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val EDIT_CELL = "edit"
|
||||||
|
const val PATH_PREFIX = "path:"
|
||||||
|
const val URL_PREFIX = "url:"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SkillSourceDialog(
|
||||||
|
value: String,
|
||||||
|
private val path: Boolean,
|
||||||
|
private val choose: (JComponent) -> String?,
|
||||||
|
) : DialogWrapper(true) {
|
||||||
|
private val field = JBTextField(value)
|
||||||
|
|
||||||
|
init {
|
||||||
|
title = if (path) KiloBundle.message("settings.agentBehavior.skills.sources.editPath.title")
|
||||||
|
else KiloBundle.message("settings.agentBehavior.skills.sources.editUrl.title")
|
||||||
|
setOKButtonText(KiloBundle.message("common.save"))
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createCenterPanel(): JComponent {
|
||||||
|
if (!path) return field.apply { columns = SOURCE_COLUMNS }
|
||||||
|
return JPanel(BorderLayout(UiStyle.Gap.sm(), 0)).apply {
|
||||||
|
add(field.apply { columns = SOURCE_COLUMNS }, BorderLayout.CENTER)
|
||||||
|
add(JButton("...").apply {
|
||||||
|
addActionListener {
|
||||||
|
choose(this)?.let { field.text = it }
|
||||||
|
}
|
||||||
|
}, BorderLayout.EAST)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun value() = field.text
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun chooseSkillPath(parent: JComponent): String? {
|
||||||
|
return FileChooser.chooseFile(skillPathDescriptor(), parent, null, null as VirtualFile?)?.path
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun skillPathDescriptor() = FileChooserDescriptor(false, true, false, false, false, false).apply {
|
||||||
|
title = KiloBundle.message("settings.agentBehavior.skills.sources.addPath.title")
|
||||||
|
description = KiloBundle.message("settings.agentBehavior.skills.sources.addPath.prompt")
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun skillFileType(location: String, content: String? = null): FileType {
|
||||||
|
val syntax = content?.syntaxName()
|
||||||
|
val name = syntax ?: location.substringAfterLast('/').substringAfterLast('\\').ifBlank { SKILL_FILE }
|
||||||
|
val type = FileTypeManager.getInstance().getFileTypeByFileName(name)
|
||||||
|
if (type == UnknownFileType.INSTANCE) return PlainTextFileType.INSTANCE
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.syntaxName(): String? {
|
||||||
|
val text = trimStart()
|
||||||
|
if (text.isBlank()) return null
|
||||||
|
if (text.looksHtml()) return "index.html"
|
||||||
|
if (text.looksMarkdown()) return SKILL_FILE
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.looksHtml() = contains(Regex("^\\s*(<!doctype\\s+html|<html\\b|<body\\b|</?(h[1-6]|p|pre|code|ul|ol|li|blockquote|br)\\b)", RegexOption.IGNORE_CASE))
|
||||||
|
|
||||||
|
private fun String.looksMarkdown() = lineSequence().any { line ->
|
||||||
|
line.matches(Regex("\\s{0,3}(#{1,6}\\s+.+|[-*+]\\s+.+|\\d+\\.\\s+.+|```.*|>\\s+.+)")) ||
|
||||||
|
line.contains(Regex("(`[^`]+`|\\[[^]]+][(][^)]+[)])"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun inputSkillUrl(title: String, prompt: String): String? = Messages.showInputDialog(
|
||||||
|
prompt,
|
||||||
|
title,
|
||||||
|
Messages.getQuestionIcon(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun editorPad() = JBUI.Borders.empty(
|
||||||
|
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_VERTICAL_PADDING),
|
||||||
|
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_HORIZONTAL_PADDING),
|
||||||
|
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_VERTICAL_PADDING),
|
||||||
|
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_HORIZONTAL_PADDING),
|
||||||
|
)
|
||||||
|
|
||||||
|
private const val SOURCE_COLUMNS = 60
|
||||||
|
private const val SKILL_FILE = "SKILL.md"
|
||||||
|
private const val SKILL_LOAD_TIMEOUT_MS = 10_000L
|
||||||
+12
-1
@@ -11,6 +11,8 @@ internal class SettingsDraftState<D>(
|
|||||||
|
|
||||||
private var base = initial
|
private var base = initial
|
||||||
private var pending: D? = null
|
private var pending: D? = null
|
||||||
|
private var stale: List<D> = emptyList()
|
||||||
|
private var applied: D? = null
|
||||||
private var save = false
|
private var save = false
|
||||||
private var err: String? = null
|
private var err: String? = null
|
||||||
|
|
||||||
@@ -29,6 +31,10 @@ internal class SettingsDraftState<D>(
|
|||||||
fun accept(next: D) {
|
fun accept(next: D) {
|
||||||
val target = pending
|
val target = pending
|
||||||
if (target == null) {
|
if (target == null) {
|
||||||
|
val done = applied
|
||||||
|
if (done != null && saved(base, done) && stale.any { saved(next, it) }) return
|
||||||
|
stale = emptyList()
|
||||||
|
applied = null
|
||||||
val prev = base
|
val prev = base
|
||||||
val edit = draft
|
val edit = draft
|
||||||
base = next
|
base = next
|
||||||
@@ -51,12 +57,15 @@ internal class SettingsDraftState<D>(
|
|||||||
|
|
||||||
fun complete(token: SettingsDraftSave<D>, returned: D) {
|
fun complete(token: SettingsDraftSave<D>, returned: D) {
|
||||||
val edit = draft
|
val edit = draft
|
||||||
val next = if (saved(returned, token.target)) returned else token.target
|
val fresh = saved(returned, token.target)
|
||||||
|
val next = if (fresh) returned else token.target
|
||||||
base = next
|
base = next
|
||||||
draft = if (saved(edit, token.target)) next else edit
|
draft = if (saved(edit, token.target)) next else edit
|
||||||
pending = null
|
pending = null
|
||||||
save = false
|
save = false
|
||||||
err = null
|
err = null
|
||||||
|
stale += token.previous
|
||||||
|
applied = token.target
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fail(token: SettingsDraftSave<D>, message: String) {
|
fun fail(token: SettingsDraftSave<D>, message: String) {
|
||||||
@@ -66,6 +75,8 @@ internal class SettingsDraftState<D>(
|
|||||||
pending = null
|
pending = null
|
||||||
save = false
|
save = false
|
||||||
err = message
|
err = message
|
||||||
|
stale = emptyList()
|
||||||
|
applied = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
@@ -8,6 +8,7 @@ import java.awt.Point
|
|||||||
import java.awt.Rectangle
|
import java.awt.Rectangle
|
||||||
import javax.swing.Icon
|
import javax.swing.Icon
|
||||||
import javax.swing.JList
|
import javax.swing.JList
|
||||||
|
import javax.swing.ListSelectionModel
|
||||||
import javax.swing.ListCellRenderer
|
import javax.swing.ListCellRenderer
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
@@ -21,6 +22,8 @@ internal data class SettingsListConfig(
|
|||||||
val height: SettingsListRowHeight,
|
val height: SettingsListRowHeight,
|
||||||
val description: Boolean = true,
|
val description: Boolean = true,
|
||||||
val descriptionIndent: Boolean = true,
|
val descriptionIndent: Boolean = true,
|
||||||
|
val tooltip: Boolean = true,
|
||||||
|
val selection: Int = ListSelectionModel.SINGLE_SELECTION,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
val Equal = SettingsListConfig(SettingsListRowHeight.EQUAL)
|
val Equal = SettingsListConfig(SettingsListRowHeight.EQUAL)
|
||||||
@@ -41,7 +44,9 @@ internal data class SettingsListCell(
|
|||||||
internal interface SettingsListItem {
|
internal interface SettingsListItem {
|
||||||
val key: String
|
val key: String
|
||||||
val title: String
|
val title: String
|
||||||
|
val note: String? get() = null
|
||||||
val description: String? get() = null
|
val description: String? get() = null
|
||||||
|
val doubleClick: String? get() = null
|
||||||
val icon: Icon? get() = null
|
val icon: Icon? get() = null
|
||||||
val section: String? get() = null
|
val section: String? get() = null
|
||||||
val badges: List<SettingsBadge> get() = emptyList()
|
val badges: List<SettingsBadge> get() = emptyList()
|
||||||
|
|||||||
+3
@@ -83,6 +83,9 @@ internal class SettingsListRenderer(
|
|||||||
|
|
||||||
title.clear()
|
title.clear()
|
||||||
title.append(value.title, SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, fg))
|
title.append(value.title, SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, fg))
|
||||||
|
value.note?.takeIf { it.isNotBlank() }?.let {
|
||||||
|
title.append(" $it", SimpleTextAttributes.GRAYED_ATTRIBUTES)
|
||||||
|
}
|
||||||
syncBadges(value)
|
syncBadges(value)
|
||||||
icon.icon = value.icon
|
icon.icon = value.icon
|
||||||
mark.isVisible = value.icon != null
|
mark.isVisible = value.icon != null
|
||||||
|
|||||||
+51
-7
@@ -1,6 +1,7 @@
|
|||||||
package ai.kilocode.client.settings.base
|
package ai.kilocode.client.settings.base
|
||||||
|
|
||||||
import ai.kilocode.client.session.ui.model.ModelSearch
|
import ai.kilocode.client.session.ui.model.ModelSearch
|
||||||
|
import ai.kilocode.client.ui.UiStyle
|
||||||
import com.intellij.openapi.application.ApplicationManager
|
import com.intellij.openapi.application.ApplicationManager
|
||||||
import com.intellij.ui.CollectionListModel
|
import com.intellij.ui.CollectionListModel
|
||||||
import com.intellij.ui.ScrollingUtil
|
import com.intellij.ui.ScrollingUtil
|
||||||
@@ -8,23 +9,27 @@ import com.intellij.ui.components.JBList
|
|||||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||||
import com.intellij.xml.util.XmlStringUtil
|
import com.intellij.xml.util.XmlStringUtil
|
||||||
import com.intellij.util.ui.UIUtil
|
import com.intellij.util.ui.UIUtil
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.Rectangle
|
||||||
import java.awt.event.KeyEvent
|
import java.awt.event.KeyEvent
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
import javax.swing.KeyStroke
|
import javax.swing.KeyStroke
|
||||||
import javax.swing.ListSelectionModel
|
import javax.swing.ListSelectionModel
|
||||||
|
import javax.swing.Scrollable
|
||||||
|
import javax.swing.SwingConstants
|
||||||
import javax.swing.event.ListSelectionEvent
|
import javax.swing.event.ListSelectionEvent
|
||||||
|
|
||||||
internal class SettingsListView(
|
internal class SettingsListView(
|
||||||
empty: String,
|
empty: String,
|
||||||
private val cfg: SettingsListConfig = SettingsListConfig.Equal,
|
private val cfg: SettingsListConfig = SettingsListConfig.Equal,
|
||||||
private val onCell: (String, String) -> Unit,
|
private val onCell: (String, String) -> Unit,
|
||||||
) : BaseContentPanel() {
|
) : BaseContentPanel(), Scrollable {
|
||||||
private val model = CollectionListModel<SettingsListItem>()
|
private val model = CollectionListModel<SettingsListItem>()
|
||||||
internal val list = object : JBList<SettingsListItem>(model) {
|
internal val list = object : JBList<SettingsListItem>(model) {
|
||||||
override fun getToolTipText(event: MouseEvent): String? {
|
override fun getToolTipText(event: MouseEvent): String? {
|
||||||
if (!cfg.description) return null
|
if (!cfg.description || !cfg.tooltip) return null
|
||||||
val idx = locationToIndex(event.point)
|
val idx = locationToIndex(event.point)
|
||||||
if (idx < 0) return null
|
if (idx < 0) return null
|
||||||
val bounds = getCellBounds(idx, idx) ?: return null
|
val bounds = getCellBounds(idx, idx) ?: return null
|
||||||
@@ -34,7 +39,7 @@ internal class SettingsListView(
|
|||||||
return XmlStringUtil.wrapInHtml(text)
|
return XmlStringUtil.wrapInHtml(text)
|
||||||
}
|
}
|
||||||
}.apply {
|
}.apply {
|
||||||
selectionMode = ListSelectionModel.SINGLE_SELECTION
|
selectionMode = cfg.selection
|
||||||
setExpandableItemsEnabled(false)
|
setExpandableItemsEnabled(false)
|
||||||
emptyText.text = empty
|
emptyText.text = empty
|
||||||
}
|
}
|
||||||
@@ -67,6 +72,11 @@ internal class SettingsListView(
|
|||||||
val hit = hit(e, enabled = false) ?: return
|
val hit = hit(e, enabled = false) ?: return
|
||||||
if (hit.id != null) return
|
if (hit.id != null) return
|
||||||
val item = hit.item
|
val item = hit.item
|
||||||
|
item.doubleClick?.let { id ->
|
||||||
|
onCell(item.key, id)
|
||||||
|
e.consume()
|
||||||
|
return
|
||||||
|
}
|
||||||
primary(item)
|
primary(item)
|
||||||
e.consume()
|
e.consume()
|
||||||
}
|
}
|
||||||
@@ -94,6 +104,12 @@ internal class SettingsListView(
|
|||||||
return list.selectedValue
|
return list.selectedValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresEdt
|
||||||
|
fun selectedItems(): List<SettingsListItem> {
|
||||||
|
checkEdt()
|
||||||
|
return list.selectedValuesList
|
||||||
|
}
|
||||||
|
|
||||||
@RequiresEdt
|
@RequiresEdt
|
||||||
fun selectedIndex(): Int {
|
fun selectedIndex(): Int {
|
||||||
checkEdt()
|
checkEdt()
|
||||||
@@ -121,6 +137,7 @@ internal class SettingsListView(
|
|||||||
@RequiresEdt
|
@RequiresEdt
|
||||||
fun setBusy(value: Boolean) {
|
fun setBusy(value: Boolean) {
|
||||||
checkEdt()
|
checkEdt()
|
||||||
|
list.setPaintBusy(value)
|
||||||
if (list.isEnabled == !value) return
|
if (list.isEnabled == !value) return
|
||||||
list.isEnabled = !value
|
list.isEnabled = !value
|
||||||
list.repaint()
|
list.repaint()
|
||||||
@@ -191,9 +208,15 @@ internal class SettingsListView(
|
|||||||
private fun primary(item: SettingsListItem) {
|
private fun primary(item: SettingsListItem) {
|
||||||
val cells = settingsListVisibleCells(item, true)
|
val cells = settingsListVisibleCells(item, true)
|
||||||
val cell = cells.firstOrNull { it.enabled && it.primary }
|
val cell = cells.firstOrNull { it.enabled && it.primary }
|
||||||
?: cells.firstOrNull { it.enabled }
|
if (cell != null) {
|
||||||
?: return
|
onCell(item.key, cell.id)
|
||||||
onCell(item.key, cell.id)
|
return
|
||||||
|
}
|
||||||
|
item.doubleClick?.let { id ->
|
||||||
|
onCell(item.key, id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cells.firstOrNull { it.enabled }?.let { onCell(item.key, it.id) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hit(e: MouseEvent, enabled: Boolean = true): Hit? {
|
private fun hit(e: MouseEvent, enabled: Boolean = true): Hit? {
|
||||||
@@ -201,7 +224,7 @@ internal class SettingsListView(
|
|||||||
val bounds = idx.takeIf { it >= 0 }?.let { list.getCellBounds(it, it) } ?: return null
|
val bounds = idx.takeIf { it >= 0 }?.let { list.getCellBounds(it, it) } ?: return null
|
||||||
if (!bounds.contains(e.point)) return null
|
if (!bounds.contains(e.point)) return null
|
||||||
val item = model.getElementAt(idx)
|
val item = model.getElementAt(idx)
|
||||||
val selected = idx == list.selectedIndex
|
val selected = list.isSelectedIndex(idx)
|
||||||
val id = if (enabled) {
|
val id = if (enabled) {
|
||||||
settingsListCellAt(list, idx, e.point, selected)
|
settingsListCellAt(list, idx, e.point, selected)
|
||||||
} else {
|
} else {
|
||||||
@@ -217,6 +240,27 @@ internal class SettingsListView(
|
|||||||
check(ApplicationManager.getApplication().isDispatchThread) { "Settings list updates must run on EDT" }
|
check(ApplicationManager.getApplication().isDispatchThread) { "Settings list updates must run on EDT" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getScrollableTracksViewportWidth() = true
|
||||||
|
|
||||||
|
override fun getScrollableTracksViewportHeight() = false
|
||||||
|
|
||||||
|
override fun getPreferredScrollableViewportSize(): Dimension = preferredSize
|
||||||
|
|
||||||
|
override fun getScrollableUnitIncrement(
|
||||||
|
visibleRect: Rectangle,
|
||||||
|
orientation: Int,
|
||||||
|
direction: Int,
|
||||||
|
): Int {
|
||||||
|
if (orientation != SwingConstants.VERTICAL) return UiStyle.Gap.pad()
|
||||||
|
return list.fixedCellHeight.takeIf { it > 0 } ?: UiStyle.Gap.xl()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getScrollableBlockIncrement(
|
||||||
|
visibleRect: Rectangle,
|
||||||
|
orientation: Int,
|
||||||
|
direction: Int,
|
||||||
|
) = if (orientation == SwingConstants.VERTICAL) visibleRect.height else visibleRect.width
|
||||||
|
|
||||||
private data class Hit(val item: SettingsListItem, val id: String?)
|
private data class Hit(val item: SettingsListItem, val id: String?)
|
||||||
|
|
||||||
private data class Press(val key: String, val id: String)
|
private data class Press(val key: String, val id: String)
|
||||||
|
|||||||
+8
@@ -33,6 +33,14 @@ internal open class SettingsPanel : SettingsOverlayPanel() {
|
|||||||
repaint()
|
repaint()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected fun setCenter(component: JComponent) {
|
||||||
|
val layout = content.layout as? BorderLayout
|
||||||
|
layout?.getLayoutComponent(BorderLayout.CENTER)?.let { content.remove(it) }
|
||||||
|
content.add(component, BorderLayout.CENTER)
|
||||||
|
revalidate()
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SettingsBody : Stack(StackAxis.VERTICAL), Scrollable {
|
private class SettingsBody : Stack(StackAxis.VERTICAL), Scrollable {
|
||||||
|
|||||||
+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
|
<applicationConfigurable
|
||||||
parentId="ai.kilocode.jetbrains.settings"
|
parentId="ai.kilocode.jetbrains.settings"
|
||||||
id="ai.kilocode.jetbrains.settings.profile"
|
id="ai.kilocode.jetbrains.settings.profile"
|
||||||
groupWeight="4"
|
groupWeight="5"
|
||||||
instance="ai.kilocode.client.settings.profile.UserProfileConfigurable"
|
instance="ai.kilocode.client.settings.profile.UserProfileConfigurable"
|
||||||
bundle="messages.KiloBundle"
|
bundle="messages.KiloBundle"
|
||||||
key="settings.profile.displayName"/>
|
key="settings.profile.displayName"/>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<applicationConfigurable
|
<applicationConfigurable
|
||||||
parentId="ai.kilocode.jetbrains.settings"
|
parentId="ai.kilocode.jetbrains.settings"
|
||||||
id="ai.kilocode.jetbrains.settings.models"
|
id="ai.kilocode.jetbrains.settings.models"
|
||||||
groupWeight="3"
|
groupWeight="4"
|
||||||
instance="ai.kilocode.client.settings.models.ModelsConfigurable"
|
instance="ai.kilocode.client.settings.models.ModelsConfigurable"
|
||||||
bundle="messages.KiloBundle"
|
bundle="messages.KiloBundle"
|
||||||
key="settings.models.displayName"/>
|
key="settings.models.displayName"/>
|
||||||
@@ -62,6 +62,14 @@
|
|||||||
bundle="messages.KiloBundle"
|
bundle="messages.KiloBundle"
|
||||||
key="settings.agentBehavior.displayName"/>
|
key="settings.agentBehavior.displayName"/>
|
||||||
|
|
||||||
|
<applicationConfigurable
|
||||||
|
parentId="ai.kilocode.jetbrains.settings"
|
||||||
|
id="ai.kilocode.jetbrains.settings.context"
|
||||||
|
groupWeight="0"
|
||||||
|
instance="ai.kilocode.client.settings.context.ContextConfigurable"
|
||||||
|
bundle="messages.KiloBundle"
|
||||||
|
key="settings.context.displayName"/>
|
||||||
|
|
||||||
<applicationConfigurable
|
<applicationConfigurable
|
||||||
parentId="ai.kilocode.jetbrains.settings.agentBehavior"
|
parentId="ai.kilocode.jetbrains.settings.agentBehavior"
|
||||||
id="ai.kilocode.jetbrains.settings.agentBehavior.agents"
|
id="ai.kilocode.jetbrains.settings.agentBehavior.agents"
|
||||||
@@ -76,6 +84,13 @@
|
|||||||
bundle="messages.KiloBundle"
|
bundle="messages.KiloBundle"
|
||||||
key="settings.agentBehavior.mcp.displayName"/>
|
key="settings.agentBehavior.mcp.displayName"/>
|
||||||
|
|
||||||
|
<applicationConfigurable
|
||||||
|
parentId="ai.kilocode.jetbrains.settings.agentBehavior"
|
||||||
|
id="ai.kilocode.jetbrains.settings.agentBehavior.skills"
|
||||||
|
instance="ai.kilocode.client.settings.agents.SkillsConfigurable"
|
||||||
|
bundle="messages.KiloBundle"
|
||||||
|
key="settings.agentBehavior.skills.displayName"/>
|
||||||
|
|
||||||
<registryKey key="kilo.session.condense"
|
<registryKey key="kilo.session.condense"
|
||||||
description="Enable event condensing in the session update queue (merges redundant snapshots before model delivery)."
|
description="Enable event condensing in the session update queue (merges redundant snapshots before model delivery)."
|
||||||
defaultValue="true"
|
defaultValue="true"
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
common.delete=Delete
|
common.delete=Delete
|
||||||
|
common.open=Open
|
||||||
|
common.save=Save
|
||||||
session.action.cancel=Cancel
|
session.action.cancel=Cancel
|
||||||
|
|
||||||
session.connection.connecting=Loading...
|
session.connection.connecting=Loading...
|
||||||
@@ -303,6 +305,27 @@ settings.kilo.description=Configure Kilo Code AI coding assistant features and a
|
|||||||
settings.cli.unavailable.title=Kilo Code is not connected
|
settings.cli.unavailable.title=Kilo Code is not connected
|
||||||
settings.cli.unavailable.message=Settings are available after Kilo Code connects to Core. If this does not resolve, restart the IDE or Kilo Core and try again.
|
settings.cli.unavailable.message=Settings are available after Kilo Code connects to Core. If this does not resolve, restart the IDE or Kilo Core and try again.
|
||||||
settings.models.displayName=Models
|
settings.models.displayName=Models
|
||||||
|
settings.context.displayName=Context
|
||||||
|
settings.context.description=Configure context behavior.
|
||||||
|
settings.context.save.pending=Saving context settings...
|
||||||
|
settings.context.save.failed=Failed to save context settings
|
||||||
|
settings.context.compaction.title=Compaction
|
||||||
|
settings.context.compaction.auto.title=Auto Compaction
|
||||||
|
settings.context.compaction.auto.description=Automatically compact context before it reaches the limit
|
||||||
|
settings.context.compaction.threshold.title=Auto Compaction Limit
|
||||||
|
settings.context.compaction.threshold.description=Compact when context reaches this percentage of the model window. Leave blank to use the safety buffer only.
|
||||||
|
settings.context.compaction.threshold.placeholder=Default
|
||||||
|
settings.context.compaction.threshold.suffix=%
|
||||||
|
settings.context.compaction.threshold.invalid=Enter a number from 0 to 100, or leave the field blank.
|
||||||
|
settings.context.compaction.prune.title=Prune Old Outputs
|
||||||
|
settings.context.compaction.prune.description=Remove old tool outputs during compaction
|
||||||
|
settings.context.watcher.title=File Watcher Ignore Patterns
|
||||||
|
settings.context.watcher.description=Glob patterns for files the watcher should ignore
|
||||||
|
settings.context.watcher.add=Add pattern
|
||||||
|
settings.context.watcher.remove=Remove selected patterns
|
||||||
|
settings.context.watcher.empty=No ignore patterns configured.
|
||||||
|
settings.context.watcher.input.title=Add ignore pattern
|
||||||
|
settings.context.watcher.input.prompt=Enter a glob pattern to ignore:
|
||||||
settings.providers.displayName=Providers
|
settings.providers.displayName=Providers
|
||||||
settings.agentBehavior.displayName=Agent Behavior
|
settings.agentBehavior.displayName=Agent Behavior
|
||||||
settings.agentBehavior.description=Configure agents, MCP servers, rules, workflows, and skills.
|
settings.agentBehavior.description=Configure agents, MCP servers, rules, workflows, and skills.
|
||||||
@@ -436,6 +459,31 @@ settings.agentBehavior.mcp.status.failed=failed
|
|||||||
settings.agentBehavior.mcp.status.needsAuth=needs auth
|
settings.agentBehavior.mcp.status.needsAuth=needs auth
|
||||||
settings.agentBehavior.mcp.status.needsRegistration=needs registration
|
settings.agentBehavior.mcp.status.needsRegistration=needs registration
|
||||||
settings.agentBehavior.mcp.status.disabled=disabled
|
settings.agentBehavior.mcp.status.disabled=disabled
|
||||||
|
settings.agentBehavior.skills.displayName=Skills
|
||||||
|
settings.agentBehavior.skills.search=Filter skills
|
||||||
|
settings.agentBehavior.skills.empty=No skills found.
|
||||||
|
settings.agentBehavior.skills.content.empty=No skill content available.
|
||||||
|
settings.agentBehavior.skills.load.timeout=Skill loading timed out. Existing skills were kept; remove slow or unreachable URLs and refresh.
|
||||||
|
settings.agentBehavior.skills.reload.deferred=Skills source saved. Reload the core after active sessions finish to apply new skills.
|
||||||
|
settings.agentBehavior.skills.reload.blocked=Skills settings saved, but active sessions are present. Reload the core after those sessions finish to apply the new skills.
|
||||||
|
settings.agentBehavior.skills.saved.notification=Skills settings saved
|
||||||
|
settings.agentBehavior.skills.delete.title=Delete Skill
|
||||||
|
settings.agentBehavior.skills.delete.message=Delete skill {0}? This removes the skill file and cannot be undone.
|
||||||
|
settings.agentBehavior.skills.delete.failed=Could not delete the skill.
|
||||||
|
settings.agentBehavior.skills.openInEditor=Open in Editor
|
||||||
|
settings.agentBehavior.skills.openInEditor.pending=The skill file will open after you close Settings.
|
||||||
|
settings.agentBehavior.skills.openInEditor.failed=Could not open the skill file in the editor.
|
||||||
|
settings.agentBehavior.skills.sources.empty=No skill sources configured.
|
||||||
|
settings.agentBehavior.skills.sources.title=Additional Skill Sources
|
||||||
|
settings.agentBehavior.skills.sources.add=Add
|
||||||
|
settings.agentBehavior.skills.sources.addPath=Add path
|
||||||
|
settings.agentBehavior.skills.sources.addPath.title=Add Skill Path
|
||||||
|
settings.agentBehavior.skills.sources.addPath.prompt=Choose a folder containing Kilo skills.
|
||||||
|
settings.agentBehavior.skills.sources.addUrl=Add URL
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.title=Add Skill URL
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.prompt=Enter a skill source URL.
|
||||||
|
settings.agentBehavior.skills.sources.editPath.title=Edit Skill Path
|
||||||
|
settings.agentBehavior.skills.sources.editUrl.title=Edit Skill URL
|
||||||
settings.providers.loading=Loading providers...
|
settings.providers.loading=Loading providers...
|
||||||
settings.providers.connected=Connected providers
|
settings.providers.connected=Connected providers
|
||||||
settings.providers.available=Available providers
|
settings.providers.available=Available providers
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
common.delete=حذف
|
common.delete=حذف
|
||||||
|
common.open=فتح
|
||||||
|
common.save=حفظ
|
||||||
session.action.cancel=إلغاء
|
session.action.cancel=إلغاء
|
||||||
session.connection.connecting=جاري التحميل…
|
session.connection.connecting=جاري التحميل…
|
||||||
session.connection.error.app=فشل الاتصال
|
session.connection.error.app=فشل الاتصال
|
||||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
|||||||
|
|
||||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||||
settings.agentBehavior.mcp.addHint=لإضافة خادم MCP، اطلب من الوكيل إضافته.
|
settings.agentBehavior.mcp.addHint=لإضافة خادم MCP، اطلب من الوكيل إضافته.
|
||||||
|
settings.agentBehavior.skills.displayName=المهارات
|
||||||
|
settings.agentBehavior.skills.search=تصفية المهارات
|
||||||
|
settings.agentBehavior.skills.empty=لم يتم العثور على مهارات.
|
||||||
|
settings.agentBehavior.skills.content.empty=لا يوجد محتوى مهارة متاح.
|
||||||
|
settings.agentBehavior.skills.load.timeout=انتهت مهلة تحميل المهارات. تم الاحتفاظ بالمهارات الحالية؛ أزل عناوين URL البطيئة أو غير المتاحة ثم حدّث.
|
||||||
|
settings.agentBehavior.skills.reload.deferred=تم حفظ مصدر المهارات. أعد تحميل Core بعد انتهاء الجلسات النشطة لتطبيق المهارات الجديدة.
|
||||||
|
settings.agentBehavior.skills.reload.blocked=تم حفظ إعدادات المهارات، لكن توجد جلسات نشطة. أعد تحميل Core بعد انتهاء تلك الجلسات لتطبيق المهارات الجديدة.
|
||||||
|
settings.agentBehavior.skills.saved.notification=تم حفظ إعدادات المهارات
|
||||||
|
settings.agentBehavior.skills.delete.title=حذف المهارة
|
||||||
|
settings.agentBehavior.skills.delete.message=هل تريد حذف المهارة {0}؟ سيؤدي ذلك إلى إزالة ملف المهارة ولا يمكن التراجع عنه.
|
||||||
|
settings.agentBehavior.skills.delete.failed=تعذر حذف المهارة.
|
||||||
|
settings.agentBehavior.skills.openInEditor=فتح في المحرر
|
||||||
|
settings.agentBehavior.skills.openInEditor.pending=سيتم فتح ملف المهارة بعد إغلاق الإعدادات.
|
||||||
|
settings.agentBehavior.skills.openInEditor.failed=تعذر فتح ملف المهارة في المحرر.
|
||||||
|
settings.agentBehavior.skills.sources.empty=لم يتم تكوين مصادر مهارات.
|
||||||
|
settings.agentBehavior.skills.sources.title=مصادر مهارات إضافية
|
||||||
|
settings.agentBehavior.skills.sources.add=إضافة
|
||||||
|
settings.agentBehavior.skills.sources.addPath=إضافة مسار
|
||||||
|
settings.agentBehavior.skills.sources.addPath.title=إضافة مسار مهارات
|
||||||
|
settings.agentBehavior.skills.sources.addPath.prompt=اختر مجلداً يحتوي على مهارات Kilo.
|
||||||
|
settings.agentBehavior.skills.sources.addUrl=إضافة URL
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.title=إضافة URL لمصدر مهارات
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.prompt=أدخل URL لمصدر مهارات.
|
||||||
|
settings.agentBehavior.skills.sources.editPath.title=تعديل مسار المهارات
|
||||||
|
settings.agentBehavior.skills.sources.editUrl.title=تعديل URL المهارات
|
||||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||||
revert.banner.count.one=تم التراجع عن رسالة واحدة
|
revert.banner.count.one=تم التراجع عن رسالة واحدة
|
||||||
@@ -376,3 +403,26 @@ revert.banner.redo=إعادة
|
|||||||
revert.banner.redo.all=إعادة الكل
|
revert.banner.redo.all=إعادة الكل
|
||||||
revert.banner.hint=يمكنك إعادة هذه التغييرات حتى ترسل رسالة جديدة
|
revert.banner.hint=يمكنك إعادة هذه التغييرات حتى ترسل رسالة جديدة
|
||||||
revert.message.rollback=العودة إلى هذه الرسالة
|
revert.message.rollback=العودة إلى هذه الرسالة
|
||||||
|
|
||||||
|
# Context settings
|
||||||
|
settings.context.displayName=السياق
|
||||||
|
settings.context.description=تكوين سلوك السياق.
|
||||||
|
settings.context.save.pending=جارٍ حفظ إعدادات السياق...
|
||||||
|
settings.context.save.failed=فشل حفظ إعدادات السياق
|
||||||
|
settings.context.compaction.title=الضغط
|
||||||
|
settings.context.compaction.auto.title=ضغط تلقائي
|
||||||
|
settings.context.compaction.auto.description=ضغط السياق تلقائياً قبل أن يصل إلى الحد
|
||||||
|
settings.context.compaction.threshold.title=حد الضغط التلقائي
|
||||||
|
settings.context.compaction.threshold.description=اضغط عندما يصل السياق إلى هذه النسبة المئوية من نافذة النموذج. اتركه فارغاً لاستخدام هامش الأمان فقط.
|
||||||
|
settings.context.compaction.threshold.placeholder=افتراضي
|
||||||
|
settings.context.compaction.threshold.suffix=%
|
||||||
|
settings.context.compaction.threshold.invalid=أدخل رقماً من 0 إلى 100، أو اترك الحقل فارغاً.
|
||||||
|
settings.context.compaction.prune.title=تقليم المخرجات القديمة
|
||||||
|
settings.context.compaction.prune.description=إزالة مخرجات الأدوات القديمة أثناء الضغط
|
||||||
|
settings.context.watcher.title=أنماط تجاهل مراقب الملفات
|
||||||
|
settings.context.watcher.description=أنماط glob للملفات التي يجب على المراقب تجاهلها
|
||||||
|
settings.context.watcher.add=إضافة نمط
|
||||||
|
settings.context.watcher.remove=إزالة الأنماط المحددة
|
||||||
|
settings.context.watcher.empty=لم يتم تكوين أنماط تجاهل.
|
||||||
|
settings.context.watcher.input.title=إضافة نمط تجاهل
|
||||||
|
settings.context.watcher.input.prompt=أدخل نمط glob لتجاهله:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
common.delete=Obriši
|
common.delete=Obriši
|
||||||
|
common.open=Otvori
|
||||||
|
common.save=Sačuvaj
|
||||||
session.action.cancel=Otkaži
|
session.action.cancel=Otkaži
|
||||||
session.connection.connecting=Učitavanje…
|
session.connection.connecting=Učitavanje…
|
||||||
session.connection.error.app=Greška pri spajanju
|
session.connection.error.app=Greška pri spajanju
|
||||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
|||||||
|
|
||||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||||
settings.agentBehavior.mcp.addHint=Da dodate MCP server, zamolite agenta da ga doda.
|
settings.agentBehavior.mcp.addHint=Da dodate MCP server, zamolite agenta da ga doda.
|
||||||
|
settings.agentBehavior.skills.displayName=Vještine
|
||||||
|
settings.agentBehavior.skills.search=Filtriraj vještine
|
||||||
|
settings.agentBehavior.skills.empty=Nema pronađenih vještina.
|
||||||
|
settings.agentBehavior.skills.content.empty=Nema dostupnog sadržaja vještine.
|
||||||
|
settings.agentBehavior.skills.load.timeout=Učitavanje vještina je isteklo. Postojeće vještine su zadržane; uklonite spore ili nedostupne URL-ove i osvježite.
|
||||||
|
settings.agentBehavior.skills.reload.deferred=Izvor vještina je sačuvan. Ponovo učitajte Core nakon što aktivne sesije završe da primijenite nove vještine.
|
||||||
|
settings.agentBehavior.skills.reload.blocked=Postavke vještina su sačuvane, ali postoje aktivne sesije. Ponovo učitajte Core nakon što te sesije završe da primijenite nove vještine.
|
||||||
|
settings.agentBehavior.skills.saved.notification=Postavke vještina su sačuvane
|
||||||
|
settings.agentBehavior.skills.delete.title=Izbriši vještinu
|
||||||
|
settings.agentBehavior.skills.delete.message=Izbrisati vještinu {0}? Ovo uklanja datoteku vještine i ne može se poništiti.
|
||||||
|
settings.agentBehavior.skills.delete.failed=Nije moguće izbrisati vještinu.
|
||||||
|
settings.agentBehavior.skills.openInEditor=Otvori u editoru
|
||||||
|
settings.agentBehavior.skills.openInEditor.pending=Datoteka vještine će se otvoriti nakon što zatvorite Postavke.
|
||||||
|
settings.agentBehavior.skills.openInEditor.failed=Nije moguće otvoriti datoteku vještine u editoru.
|
||||||
|
settings.agentBehavior.skills.sources.empty=Nema konfigurisanih izvora vještina.
|
||||||
|
settings.agentBehavior.skills.sources.title=Dodatni izvori vještina
|
||||||
|
settings.agentBehavior.skills.sources.add=Dodaj
|
||||||
|
settings.agentBehavior.skills.sources.addPath=Dodaj putanju
|
||||||
|
settings.agentBehavior.skills.sources.addPath.title=Dodaj putanju vještina
|
||||||
|
settings.agentBehavior.skills.sources.addPath.prompt=Odaberite folder koji sadrži Kilo vještine.
|
||||||
|
settings.agentBehavior.skills.sources.addUrl=Dodaj URL
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.title=Dodaj URL izvora vještina
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.prompt=Unesite URL izvora vještina.
|
||||||
|
settings.agentBehavior.skills.sources.editPath.title=Uredi putanju vještina
|
||||||
|
settings.agentBehavior.skills.sources.editUrl.title=Uredi URL vještina
|
||||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||||
revert.banner.count.one={0} poruka vraćena
|
revert.banner.count.one={0} poruka vraćena
|
||||||
@@ -376,3 +403,26 @@ revert.banner.redo=Ponovi
|
|||||||
revert.banner.redo.all=Ponovi sve
|
revert.banner.redo.all=Ponovi sve
|
||||||
revert.banner.hint=Možete ponoviti ove promjene dok ne pošaljete novu poruku
|
revert.banner.hint=Možete ponoviti ove promjene dok ne pošaljete novu poruku
|
||||||
revert.message.rollback=Vrati na ovu poruku
|
revert.message.rollback=Vrati na ovu poruku
|
||||||
|
|
||||||
|
# Context settings
|
||||||
|
settings.context.displayName=Kontekst
|
||||||
|
settings.context.description=Konfigurišite ponašanje konteksta.
|
||||||
|
settings.context.save.pending=Spremanje postavki konteksta...
|
||||||
|
settings.context.save.failed=Spremanje postavki konteksta nije uspjelo
|
||||||
|
settings.context.compaction.title=Kompresija
|
||||||
|
settings.context.compaction.auto.title=Automatska kompresija
|
||||||
|
settings.context.compaction.auto.description=Automatski komprimiraj kontekst prije nego dostigne limit
|
||||||
|
settings.context.compaction.threshold.title=Limit automatske kompresije
|
||||||
|
settings.context.compaction.threshold.description=Komprimiraj kada kontekst dostigne ovaj procenat prozora modela. Ostavite prazno da koristite samo sigurnosnu rezervu.
|
||||||
|
settings.context.compaction.threshold.placeholder=Zadano
|
||||||
|
settings.context.compaction.threshold.suffix=%
|
||||||
|
settings.context.compaction.threshold.invalid=Unesite broj od 0 do 100 ili ostavite polje prazno.
|
||||||
|
settings.context.compaction.prune.title=Očisti stare izlaze
|
||||||
|
settings.context.compaction.prune.description=Ukloni stare izlaze alata tokom kompresije
|
||||||
|
settings.context.watcher.title=Uzorci ignoriranja za promatrač datoteka
|
||||||
|
settings.context.watcher.description=Glob uzorci za datoteke koje promatrač treba ignorirati
|
||||||
|
settings.context.watcher.add=Dodaj uzorak
|
||||||
|
settings.context.watcher.remove=Ukloni odabrane uzorke
|
||||||
|
settings.context.watcher.empty=Nema konfigurisanih uzoraka za ignoriranje.
|
||||||
|
settings.context.watcher.input.title=Dodaj uzorak za ignoriranje
|
||||||
|
settings.context.watcher.input.prompt=Unesite glob uzorak za ignoriranje:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
common.delete=Slet
|
common.delete=Slet
|
||||||
|
common.open=Åbn
|
||||||
|
common.save=Gem
|
||||||
session.action.cancel=Annuller
|
session.action.cancel=Annuller
|
||||||
session.connection.connecting=Indlæser…
|
session.connection.connecting=Indlæser…
|
||||||
session.connection.error.app=Forbindelsesfejl
|
session.connection.error.app=Forbindelsesfejl
|
||||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
|||||||
|
|
||||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||||
settings.agentBehavior.mcp.addHint=For at tilføje en MCP-server skal du bede agenten om at gøre det.
|
settings.agentBehavior.mcp.addHint=For at tilføje en MCP-server skal du bede agenten om at gøre det.
|
||||||
|
settings.agentBehavior.skills.displayName=Færdigheder
|
||||||
|
settings.agentBehavior.skills.search=Filtrer færdigheder
|
||||||
|
settings.agentBehavior.skills.empty=Ingen færdigheder fundet.
|
||||||
|
settings.agentBehavior.skills.content.empty=Intet færdighedsindhold tilgængeligt.
|
||||||
|
settings.agentBehavior.skills.load.timeout=Indlæsning af færdigheder fik timeout. Eksisterende færdigheder blev bevaret; fjern langsomme eller utilgængelige URL'er og opdater.
|
||||||
|
settings.agentBehavior.skills.reload.deferred=Færdighedskilden blev gemt. Genindlæs Core, når aktive sessioner er afsluttet, for at anvende nye færdigheder.
|
||||||
|
settings.agentBehavior.skills.reload.blocked=Færdighedsindstillingerne blev gemt, men der er aktive sessioner. Genindlæs Core, når disse sessioner er afsluttet, for at anvende de nye færdigheder.
|
||||||
|
settings.agentBehavior.skills.saved.notification=Færdighedsindstillinger gemt
|
||||||
|
settings.agentBehavior.skills.delete.title=Slet færdighed
|
||||||
|
settings.agentBehavior.skills.delete.message=Slet færdigheden {0}? Dette fjerner færdighedsfilen og kan ikke fortrydes.
|
||||||
|
settings.agentBehavior.skills.delete.failed=Kunne ikke slette færdigheden.
|
||||||
|
settings.agentBehavior.skills.openInEditor=Åbn i editor
|
||||||
|
settings.agentBehavior.skills.openInEditor.pending=Færdighedsfilen åbnes, når du lukker Indstillinger.
|
||||||
|
settings.agentBehavior.skills.openInEditor.failed=Kunne ikke åbne færdighedsfilen i editoren.
|
||||||
|
settings.agentBehavior.skills.sources.empty=Ingen færdighedskilder konfigureret.
|
||||||
|
settings.agentBehavior.skills.sources.title=Yderligere færdighedskilder
|
||||||
|
settings.agentBehavior.skills.sources.add=Tilføj
|
||||||
|
settings.agentBehavior.skills.sources.addPath=Tilføj sti
|
||||||
|
settings.agentBehavior.skills.sources.addPath.title=Tilføj færdighedssti
|
||||||
|
settings.agentBehavior.skills.sources.addPath.prompt=Vælg en mappe, der indeholder Kilo-færdigheder.
|
||||||
|
settings.agentBehavior.skills.sources.addUrl=Tilføj URL
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.title=Tilføj URL til færdighedskilde
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.prompt=Indtast en URL til en færdighedskilde.
|
||||||
|
settings.agentBehavior.skills.sources.editPath.title=Rediger færdighedssti
|
||||||
|
settings.agentBehavior.skills.sources.editUrl.title=Rediger færdigheds-URL
|
||||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||||
revert.banner.count.one={0} besked rullet tilbage
|
revert.banner.count.one={0} besked rullet tilbage
|
||||||
@@ -376,3 +403,26 @@ revert.banner.redo=Gentag
|
|||||||
revert.banner.redo.all=Gentag alle
|
revert.banner.redo.all=Gentag alle
|
||||||
revert.banner.hint=Du kan gentage disse ændringer, indtil du sender en ny besked
|
revert.banner.hint=Du kan gentage disse ændringer, indtil du sender en ny besked
|
||||||
revert.message.rollback=Rul tilbage til denne besked
|
revert.message.rollback=Rul tilbage til denne besked
|
||||||
|
|
||||||
|
# Context settings
|
||||||
|
settings.context.displayName=Kontekst
|
||||||
|
settings.context.description=Konfigurer kontekstadfærd.
|
||||||
|
settings.context.save.pending=Gemmer kontekstindstillinger...
|
||||||
|
settings.context.save.failed=Kunne ikke gemme kontekstindstillinger
|
||||||
|
settings.context.compaction.title=Komprimering
|
||||||
|
settings.context.compaction.auto.title=Automatisk komprimering
|
||||||
|
settings.context.compaction.auto.description=Komprimér automatisk kontekst, før den når grænsen
|
||||||
|
settings.context.compaction.threshold.title=Grænse for automatisk komprimering
|
||||||
|
settings.context.compaction.threshold.description=Komprimér, når konteksten når denne procentdel af modelvinduet. Lad feltet være tomt for kun at bruge sikkerhedsbufferen.
|
||||||
|
settings.context.compaction.threshold.placeholder=Standard
|
||||||
|
settings.context.compaction.threshold.suffix=%
|
||||||
|
settings.context.compaction.threshold.invalid=Indtast et tal fra 0 til 100, eller lad feltet være tomt.
|
||||||
|
settings.context.compaction.prune.title=Fjern gamle output
|
||||||
|
settings.context.compaction.prune.description=Fjern gamle værktøjsoutput under komprimering
|
||||||
|
settings.context.watcher.title=Filvagt-ignormønstre
|
||||||
|
settings.context.watcher.description=Glob-mønstre for filer, som vagten skal ignorere
|
||||||
|
settings.context.watcher.add=Tilføj mønster
|
||||||
|
settings.context.watcher.remove=Fjern valgte mønstre
|
||||||
|
settings.context.watcher.empty=Ingen ignormønstre konfigureret.
|
||||||
|
settings.context.watcher.input.title=Tilføj ignormønster
|
||||||
|
settings.context.watcher.input.prompt=Indtast et glob-mønster, der skal ignoreres:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
common.delete=Löschen
|
common.delete=Löschen
|
||||||
|
common.open=Öffnen
|
||||||
|
common.save=Speichern
|
||||||
session.action.cancel=Abbrechen
|
session.action.cancel=Abbrechen
|
||||||
session.connection.connecting=Wird geladen…
|
session.connection.connecting=Wird geladen…
|
||||||
session.connection.error.app=Verbindung fehlgeschlagen
|
session.connection.error.app=Verbindung fehlgeschlagen
|
||||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
|||||||
|
|
||||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||||
settings.agentBehavior.mcp.addHint=Um einen MCP-Server hinzuzufügen, bitten Sie den Agenten darum.
|
settings.agentBehavior.mcp.addHint=Um einen MCP-Server hinzuzufügen, bitten Sie den Agenten darum.
|
||||||
|
settings.agentBehavior.skills.displayName=Skills
|
||||||
|
settings.agentBehavior.skills.search=Skills filtern
|
||||||
|
settings.agentBehavior.skills.empty=Keine Skills gefunden.
|
||||||
|
settings.agentBehavior.skills.content.empty=Keine Skill-Inhalte verfügbar.
|
||||||
|
settings.agentBehavior.skills.load.timeout=Das Laden der Skills ist abgelaufen. Vorhandene Skills wurden beibehalten; entfernen Sie langsame oder nicht erreichbare URLs und aktualisieren Sie.
|
||||||
|
settings.agentBehavior.skills.reload.deferred=Skill-Quelle gespeichert. Laden Sie Core neu, nachdem aktive Sitzungen beendet sind, um neue Skills anzuwenden.
|
||||||
|
settings.agentBehavior.skills.reload.blocked=Skill-Einstellungen gespeichert, aber es sind aktive Sitzungen vorhanden. Laden Sie Core neu, nachdem diese Sitzungen beendet sind, um neue Skills anzuwenden.
|
||||||
|
settings.agentBehavior.skills.saved.notification=Skill-Einstellungen gespeichert
|
||||||
|
settings.agentBehavior.skills.delete.title=Skill löschen
|
||||||
|
settings.agentBehavior.skills.delete.message=Skill {0} löschen? Dadurch wird die Skill-Datei entfernt und kann nicht rückgängig gemacht werden.
|
||||||
|
settings.agentBehavior.skills.delete.failed=Der Skill konnte nicht gelöscht werden.
|
||||||
|
settings.agentBehavior.skills.openInEditor=Im Editor öffnen
|
||||||
|
settings.agentBehavior.skills.openInEditor.pending=Die Skill-Datei wird geöffnet, nachdem Sie die Einstellungen geschlossen haben.
|
||||||
|
settings.agentBehavior.skills.openInEditor.failed=Die Skill-Datei konnte nicht im Editor geöffnet werden.
|
||||||
|
settings.agentBehavior.skills.sources.empty=Keine Skill-Quellen konfiguriert.
|
||||||
|
settings.agentBehavior.skills.sources.title=Zusätzliche Skill-Quellen
|
||||||
|
settings.agentBehavior.skills.sources.add=Hinzufügen
|
||||||
|
settings.agentBehavior.skills.sources.addPath=Pfad hinzufügen
|
||||||
|
settings.agentBehavior.skills.sources.addPath.title=Skill-Pfad hinzufügen
|
||||||
|
settings.agentBehavior.skills.sources.addPath.prompt=Wählen Sie einen Ordner mit Kilo-Skills aus.
|
||||||
|
settings.agentBehavior.skills.sources.addUrl=URL hinzufügen
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.title=Skill-Quellen-URL hinzufügen
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.prompt=Geben Sie eine Skill-Quellen-URL ein.
|
||||||
|
settings.agentBehavior.skills.sources.editPath.title=Skill-Pfad bearbeiten
|
||||||
|
settings.agentBehavior.skills.sources.editUrl.title=Skill-URL bearbeiten
|
||||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||||
revert.banner.count.one={0} Nachricht zurückgesetzt
|
revert.banner.count.one={0} Nachricht zurückgesetzt
|
||||||
@@ -376,3 +403,26 @@ revert.banner.redo=Wiederholen
|
|||||||
revert.banner.redo.all=Alle wiederholen
|
revert.banner.redo.all=Alle wiederholen
|
||||||
revert.banner.hint=Du kannst diese Änderungen wiederholen, bis du eine neue Nachricht sendest
|
revert.banner.hint=Du kannst diese Änderungen wiederholen, bis du eine neue Nachricht sendest
|
||||||
revert.message.rollback=Auf diese Nachricht zurücksetzen
|
revert.message.rollback=Auf diese Nachricht zurücksetzen
|
||||||
|
|
||||||
|
# Context settings
|
||||||
|
settings.context.displayName=Kontext
|
||||||
|
settings.context.description=Kontextverhalten konfigurieren.
|
||||||
|
settings.context.save.pending=Kontexteinstellungen werden gespeichert...
|
||||||
|
settings.context.save.failed=Kontexteinstellungen konnten nicht gespeichert werden
|
||||||
|
settings.context.compaction.title=Komprimierung
|
||||||
|
settings.context.compaction.auto.title=Automatische Komprimierung
|
||||||
|
settings.context.compaction.auto.description=Kontext automatisch komprimieren, bevor er das Limit erreicht
|
||||||
|
settings.context.compaction.threshold.title=Limit für automatische Komprimierung
|
||||||
|
settings.context.compaction.threshold.description=Komprimieren, wenn der Kontext diesen Prozentsatz des Modellfensters erreicht. Leer lassen, um nur den Sicherheitspuffer zu verwenden.
|
||||||
|
settings.context.compaction.threshold.placeholder=Standard
|
||||||
|
settings.context.compaction.threshold.suffix=%
|
||||||
|
settings.context.compaction.threshold.invalid=Geben Sie eine Zahl von 0 bis 100 ein oder lassen Sie das Feld leer.
|
||||||
|
settings.context.compaction.prune.title=Alte Ausgaben bereinigen
|
||||||
|
settings.context.compaction.prune.description=Alte Werkzeugausgaben während der Komprimierung entfernen
|
||||||
|
settings.context.watcher.title=Datei-Watcher-Ignorierungsmuster
|
||||||
|
settings.context.watcher.description=Glob-Muster für Dateien, die der Watcher ignorieren soll
|
||||||
|
settings.context.watcher.add=Muster hinzufügen
|
||||||
|
settings.context.watcher.remove=Ausgewählte Muster entfernen
|
||||||
|
settings.context.watcher.empty=Keine Ignorierungsmuster konfiguriert.
|
||||||
|
settings.context.watcher.input.title=Ignorierungsmuster hinzufügen
|
||||||
|
settings.context.watcher.input.prompt=Glob-Muster zum Ignorieren eingeben:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
common.delete=Eliminar
|
common.delete=Eliminar
|
||||||
|
common.open=Abrir
|
||||||
|
common.save=Guardar
|
||||||
session.action.cancel=Cancelar
|
session.action.cancel=Cancelar
|
||||||
session.connection.connecting=Cargando…
|
session.connection.connecting=Cargando…
|
||||||
session.connection.error.app=Error de conexión
|
session.connection.error.app=Error de conexión
|
||||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
|||||||
|
|
||||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||||
settings.agentBehavior.mcp.addHint=Para agregar un servidor MCP, pídele al agente que lo haga.
|
settings.agentBehavior.mcp.addHint=Para agregar un servidor MCP, pídele al agente que lo haga.
|
||||||
|
settings.agentBehavior.skills.displayName=Habilidades
|
||||||
|
settings.agentBehavior.skills.search=Filtrar habilidades
|
||||||
|
settings.agentBehavior.skills.empty=No se encontraron habilidades.
|
||||||
|
settings.agentBehavior.skills.content.empty=No hay contenido de habilidad disponible.
|
||||||
|
settings.agentBehavior.skills.load.timeout=Se agotó el tiempo de carga de habilidades. Se conservaron las habilidades existentes; elimina las URL lentas o inaccesibles y actualiza.
|
||||||
|
settings.agentBehavior.skills.reload.deferred=Fuente de habilidades guardada. Recarga Core cuando terminen las sesiones activas para aplicar nuevas habilidades.
|
||||||
|
settings.agentBehavior.skills.reload.blocked=Configuración de habilidades guardada, pero hay sesiones activas. Recarga Core cuando esas sesiones terminen para aplicar las nuevas habilidades.
|
||||||
|
settings.agentBehavior.skills.saved.notification=Configuración de habilidades guardada
|
||||||
|
settings.agentBehavior.skills.delete.title=Eliminar habilidad
|
||||||
|
settings.agentBehavior.skills.delete.message=¿Eliminar la habilidad {0}? Esto elimina el archivo de la habilidad y no se puede deshacer.
|
||||||
|
settings.agentBehavior.skills.delete.failed=No se pudo eliminar la habilidad.
|
||||||
|
settings.agentBehavior.skills.openInEditor=Abrir en el editor
|
||||||
|
settings.agentBehavior.skills.openInEditor.pending=El archivo de la habilidad se abrirá después de cerrar Configuración.
|
||||||
|
settings.agentBehavior.skills.openInEditor.failed=No se pudo abrir el archivo de la habilidad en el editor.
|
||||||
|
settings.agentBehavior.skills.sources.empty=No hay fuentes de habilidades configuradas.
|
||||||
|
settings.agentBehavior.skills.sources.title=Fuentes de habilidades adicionales
|
||||||
|
settings.agentBehavior.skills.sources.add=Agregar
|
||||||
|
settings.agentBehavior.skills.sources.addPath=Agregar ruta
|
||||||
|
settings.agentBehavior.skills.sources.addPath.title=Agregar ruta de habilidades
|
||||||
|
settings.agentBehavior.skills.sources.addPath.prompt=Elige una carpeta que contenga habilidades de Kilo.
|
||||||
|
settings.agentBehavior.skills.sources.addUrl=Agregar URL
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.title=Agregar URL de fuente de habilidades
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.prompt=Introduce una URL de fuente de habilidades.
|
||||||
|
settings.agentBehavior.skills.sources.editPath.title=Editar ruta de habilidades
|
||||||
|
settings.agentBehavior.skills.sources.editUrl.title=Editar URL de habilidades
|
||||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||||
revert.banner.count.one={0} mensaje revertido
|
revert.banner.count.one={0} mensaje revertido
|
||||||
@@ -376,3 +403,26 @@ revert.banner.redo=Rehacer
|
|||||||
revert.banner.redo.all=Rehacer todo
|
revert.banner.redo.all=Rehacer todo
|
||||||
revert.banner.hint=Puedes rehacer estos cambios hasta que envíes un mensaje nuevo
|
revert.banner.hint=Puedes rehacer estos cambios hasta que envíes un mensaje nuevo
|
||||||
revert.message.rollback=Revertir a este mensaje
|
revert.message.rollback=Revertir a este mensaje
|
||||||
|
|
||||||
|
# Context settings
|
||||||
|
settings.context.displayName=Contexto
|
||||||
|
settings.context.description=Configura el comportamiento del contexto.
|
||||||
|
settings.context.save.pending=Guardando configuración de contexto...
|
||||||
|
settings.context.save.failed=Error al guardar la configuración de contexto
|
||||||
|
settings.context.compaction.title=Compactación
|
||||||
|
settings.context.compaction.auto.title=Compactación automática
|
||||||
|
settings.context.compaction.auto.description=Compactar automáticamente el contexto antes de que alcance el límite
|
||||||
|
settings.context.compaction.threshold.title=Límite de compactación automática
|
||||||
|
settings.context.compaction.threshold.description=Compactar cuando el contexto alcance este porcentaje de la ventana del modelo. Déjalo en blanco para usar solo el búfer de seguridad.
|
||||||
|
settings.context.compaction.threshold.placeholder=Predeterminado
|
||||||
|
settings.context.compaction.threshold.suffix=%
|
||||||
|
settings.context.compaction.threshold.invalid=Introduce un número de 0 a 100 o deja el campo en blanco.
|
||||||
|
settings.context.compaction.prune.title=Eliminar salidas antiguas
|
||||||
|
settings.context.compaction.prune.description=Eliminar salidas de herramientas antiguas durante la compactación
|
||||||
|
settings.context.watcher.title=Patrones de ignorar del observador
|
||||||
|
settings.context.watcher.description=Patrones glob para archivos que el observador debe ignorar
|
||||||
|
settings.context.watcher.add=Agregar patrón
|
||||||
|
settings.context.watcher.remove=Eliminar patrones seleccionados
|
||||||
|
settings.context.watcher.empty=No hay patrones de ignorar configurados.
|
||||||
|
settings.context.watcher.input.title=Agregar patrón de ignorar
|
||||||
|
settings.context.watcher.input.prompt=Introduce un patrón glob para ignorar:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
common.delete=Supprimer
|
common.delete=Supprimer
|
||||||
|
common.open=Ouvrir
|
||||||
|
common.save=Enregistrer
|
||||||
session.action.cancel=Annuler
|
session.action.cancel=Annuler
|
||||||
session.connection.connecting=Chargement…
|
session.connection.connecting=Chargement…
|
||||||
session.connection.error.app=Échec de la connexion
|
session.connection.error.app=Échec de la connexion
|
||||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
|||||||
|
|
||||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||||
settings.agentBehavior.mcp.addHint=Pour ajouter un serveur MCP, demandez à l’agent de le faire.
|
settings.agentBehavior.mcp.addHint=Pour ajouter un serveur MCP, demandez à l’agent de le faire.
|
||||||
|
settings.agentBehavior.skills.displayName=Compétences
|
||||||
|
settings.agentBehavior.skills.search=Filtrer les compétences
|
||||||
|
settings.agentBehavior.skills.empty=Aucune compétence trouvée.
|
||||||
|
settings.agentBehavior.skills.content.empty=Aucun contenu de compétence disponible.
|
||||||
|
settings.agentBehavior.skills.load.timeout=Le chargement des compétences a expiré. Les compétences existantes ont été conservées ; supprimez les URL lentes ou inaccessibles puis actualisez.
|
||||||
|
settings.agentBehavior.skills.reload.deferred=Source de compétences enregistrée. Rechargez Core après la fin des sessions actives pour appliquer les nouvelles compétences.
|
||||||
|
settings.agentBehavior.skills.reload.blocked=Paramètres des compétences enregistrés, mais des sessions sont actives. Rechargez Core après la fin de ces sessions pour appliquer les nouvelles compétences.
|
||||||
|
settings.agentBehavior.skills.saved.notification=Paramètres des compétences enregistrés
|
||||||
|
settings.agentBehavior.skills.delete.title=Supprimer la compétence
|
||||||
|
settings.agentBehavior.skills.delete.message=Supprimer la compétence {0} ? Cela supprime le fichier de compétence et ne peut pas être annulé.
|
||||||
|
settings.agentBehavior.skills.delete.failed=Impossible de supprimer la compétence.
|
||||||
|
settings.agentBehavior.skills.openInEditor=Ouvrir dans l’éditeur
|
||||||
|
settings.agentBehavior.skills.openInEditor.pending=Le fichier de compétence s’ouvrira après la fermeture des paramètres.
|
||||||
|
settings.agentBehavior.skills.openInEditor.failed=Impossible d’ouvrir le fichier de compétence dans l’éditeur.
|
||||||
|
settings.agentBehavior.skills.sources.empty=Aucune source de compétences configurée.
|
||||||
|
settings.agentBehavior.skills.sources.title=Sources de compétences supplémentaires
|
||||||
|
settings.agentBehavior.skills.sources.add=Ajouter
|
||||||
|
settings.agentBehavior.skills.sources.addPath=Ajouter un chemin
|
||||||
|
settings.agentBehavior.skills.sources.addPath.title=Ajouter un chemin de compétences
|
||||||
|
settings.agentBehavior.skills.sources.addPath.prompt=Choisissez un dossier contenant des compétences Kilo.
|
||||||
|
settings.agentBehavior.skills.sources.addUrl=Ajouter une URL
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.title=Ajouter une URL de source de compétences
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.prompt=Saisissez une URL de source de compétences.
|
||||||
|
settings.agentBehavior.skills.sources.editPath.title=Modifier le chemin des compétences
|
||||||
|
settings.agentBehavior.skills.sources.editUrl.title=Modifier l’URL des compétences
|
||||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||||
revert.banner.count.one={0} message annulé
|
revert.banner.count.one={0} message annulé
|
||||||
@@ -376,3 +403,26 @@ revert.banner.redo=Rétablir
|
|||||||
revert.banner.redo.all=Tout rétablir
|
revert.banner.redo.all=Tout rétablir
|
||||||
revert.banner.hint=Vous pouvez rétablir ces modifications jusqu'à l'envoi d'un nouveau message
|
revert.banner.hint=Vous pouvez rétablir ces modifications jusqu'à l'envoi d'un nouveau message
|
||||||
revert.message.rollback=Revenir à ce message
|
revert.message.rollback=Revenir à ce message
|
||||||
|
|
||||||
|
# Context settings
|
||||||
|
settings.context.displayName=Contexte
|
||||||
|
settings.context.description=Configurer le comportement du contexte.
|
||||||
|
settings.context.save.pending=Enregistrement des paramètres de contexte...
|
||||||
|
settings.context.save.failed=Échec de 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 :
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
common.delete=削除
|
common.delete=削除
|
||||||
|
common.open=開く
|
||||||
|
common.save=保存
|
||||||
session.action.cancel=キャンセル
|
session.action.cancel=キャンセル
|
||||||
session.connection.connecting=読み込み中…
|
session.connection.connecting=読み込み中…
|
||||||
session.connection.error.app=接続に失敗しました
|
session.connection.error.app=接続に失敗しました
|
||||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
|||||||
|
|
||||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||||
settings.agentBehavior.mcp.addHint=MCPサーバーを追加するには、エージェントに依頼してください。
|
settings.agentBehavior.mcp.addHint=MCPサーバーを追加するには、エージェントに依頼してください。
|
||||||
|
settings.agentBehavior.skills.displayName=スキル
|
||||||
|
settings.agentBehavior.skills.search=スキルを絞り込み
|
||||||
|
settings.agentBehavior.skills.empty=スキルが見つかりません。
|
||||||
|
settings.agentBehavior.skills.content.empty=利用可能なスキル内容がありません。
|
||||||
|
settings.agentBehavior.skills.load.timeout=スキルの読み込みがタイムアウトしました。既存のスキルは保持されました。遅い、または到達不能な URL を削除して更新してください。
|
||||||
|
settings.agentBehavior.skills.reload.deferred=スキルソースを保存しました。新しいスキルを適用するには、アクティブなセッションが終了した後に Core を再読み込みしてください。
|
||||||
|
settings.agentBehavior.skills.reload.blocked=スキル設定を保存しましたが、アクティブなセッションがあります。新しいスキルを適用するには、それらのセッションが終了した後に Core を再読み込みしてください。
|
||||||
|
settings.agentBehavior.skills.saved.notification=スキル設定を保存しました
|
||||||
|
settings.agentBehavior.skills.delete.title=スキルを削除
|
||||||
|
settings.agentBehavior.skills.delete.message=スキル {0} を削除しますか?スキルファイルが削除され、この操作は元に戻せません。
|
||||||
|
settings.agentBehavior.skills.delete.failed=スキルを削除できませんでした。
|
||||||
|
settings.agentBehavior.skills.openInEditor=エディターで開く
|
||||||
|
settings.agentBehavior.skills.openInEditor.pending=設定を閉じるとスキルファイルが開きます。
|
||||||
|
settings.agentBehavior.skills.openInEditor.failed=エディターでスキルファイルを開けませんでした。
|
||||||
|
settings.agentBehavior.skills.sources.empty=スキルソースが設定されていません。
|
||||||
|
settings.agentBehavior.skills.sources.title=追加のスキルソース
|
||||||
|
settings.agentBehavior.skills.sources.add=追加
|
||||||
|
settings.agentBehavior.skills.sources.addPath=パスを追加
|
||||||
|
settings.agentBehavior.skills.sources.addPath.title=スキルパスを追加
|
||||||
|
settings.agentBehavior.skills.sources.addPath.prompt=Kilo スキルを含むフォルダーを選択してください。
|
||||||
|
settings.agentBehavior.skills.sources.addUrl=URL を追加
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.title=スキルソース URL を追加
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.prompt=スキルソース URL を入力してください。
|
||||||
|
settings.agentBehavior.skills.sources.editPath.title=スキルパスを編集
|
||||||
|
settings.agentBehavior.skills.sources.editUrl.title=スキル URL を編集
|
||||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||||
revert.banner.count.one={0} 件のメッセージをロールバックしました
|
revert.banner.count.one={0} 件のメッセージをロールバックしました
|
||||||
@@ -376,3 +403,26 @@ revert.banner.redo=やり直し
|
|||||||
revert.banner.redo.all=すべてやり直し
|
revert.banner.redo.all=すべてやり直し
|
||||||
revert.banner.hint=新しいメッセージを送信するまで、これらの変更をやり直せます
|
revert.banner.hint=新しいメッセージを送信するまで、これらの変更をやり直せます
|
||||||
revert.message.rollback=このメッセージまでロールバック
|
revert.message.rollback=このメッセージまでロールバック
|
||||||
|
|
||||||
|
# Context settings
|
||||||
|
settings.context.displayName=コンテキスト
|
||||||
|
settings.context.description=コンテキストの動作を設定します。
|
||||||
|
settings.context.save.pending=コンテキスト設定を保存しています...
|
||||||
|
settings.context.save.failed=コンテキスト設定の保存に失敗しました
|
||||||
|
settings.context.compaction.title=圧縮
|
||||||
|
settings.context.compaction.auto.title=自動圧縮
|
||||||
|
settings.context.compaction.auto.description=コンテキストが上限に達する前に自動的に圧縮
|
||||||
|
settings.context.compaction.threshold.title=自動圧縮の上限
|
||||||
|
settings.context.compaction.threshold.description=コンテキストがモデルウィンドウのこの割合に達したら圧縮します。安全バッファーのみを使用するには空欄のままにしてください。
|
||||||
|
settings.context.compaction.threshold.placeholder=デフォルト
|
||||||
|
settings.context.compaction.threshold.suffix=%
|
||||||
|
settings.context.compaction.threshold.invalid=0から100までの数値を入力するか、空欄のままにしてください。
|
||||||
|
settings.context.compaction.prune.title=古い出力を削除
|
||||||
|
settings.context.compaction.prune.description=圧縮時に古いツール出力を削除
|
||||||
|
settings.context.watcher.title=ファイルウォッチャー無視パターン
|
||||||
|
settings.context.watcher.description=ウォッチャーが無視すべきファイルのglobパターン
|
||||||
|
settings.context.watcher.add=パターンを追加
|
||||||
|
settings.context.watcher.remove=選択したパターンを削除
|
||||||
|
settings.context.watcher.empty=無視パターンは設定されていません。
|
||||||
|
settings.context.watcher.input.title=無視パターンを追加
|
||||||
|
settings.context.watcher.input.prompt=無視するglobパターンを入力してください:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
common.delete=삭제
|
common.delete=삭제
|
||||||
|
common.open=열기
|
||||||
|
common.save=저장
|
||||||
session.action.cancel=취소
|
session.action.cancel=취소
|
||||||
session.connection.connecting=로딩 중…
|
session.connection.connecting=로딩 중…
|
||||||
session.connection.error.app=연결 실패
|
session.connection.error.app=연결 실패
|
||||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
|||||||
|
|
||||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||||
settings.agentBehavior.mcp.addHint=MCP 서버를 추가하려면 에이전트에게 요청하세요.
|
settings.agentBehavior.mcp.addHint=MCP 서버를 추가하려면 에이전트에게 요청하세요.
|
||||||
|
settings.agentBehavior.skills.displayName=스킬
|
||||||
|
settings.agentBehavior.skills.search=스킬 필터링
|
||||||
|
settings.agentBehavior.skills.empty=스킬을 찾을 수 없습니다.
|
||||||
|
settings.agentBehavior.skills.content.empty=사용 가능한 스킬 내용이 없습니다.
|
||||||
|
settings.agentBehavior.skills.load.timeout=스킬 로드 시간이 초과되었습니다. 기존 스킬은 유지되었습니다. 느리거나 연결할 수 없는 URL을 제거한 뒤 새로 고치세요.
|
||||||
|
settings.agentBehavior.skills.reload.deferred=스킬 소스가 저장되었습니다. 새 스킬을 적용하려면 활성 세션이 끝난 뒤 Core를 다시 로드하세요.
|
||||||
|
settings.agentBehavior.skills.reload.blocked=스킬 설정이 저장되었지만 활성 세션이 있습니다. 새 스킬을 적용하려면 해당 세션이 끝난 뒤 Core를 다시 로드하세요.
|
||||||
|
settings.agentBehavior.skills.saved.notification=스킬 설정이 저장되었습니다
|
||||||
|
settings.agentBehavior.skills.delete.title=스킬 삭제
|
||||||
|
settings.agentBehavior.skills.delete.message=스킬 {0}을 삭제할까요? 스킬 파일이 제거되며 되돌릴 수 없습니다.
|
||||||
|
settings.agentBehavior.skills.delete.failed=스킬을 삭제할 수 없습니다.
|
||||||
|
settings.agentBehavior.skills.openInEditor=에디터에서 열기
|
||||||
|
settings.agentBehavior.skills.openInEditor.pending=설정을 닫으면 스킬 파일이 열립니다.
|
||||||
|
settings.agentBehavior.skills.openInEditor.failed=에디터에서 스킬 파일을 열 수 없습니다.
|
||||||
|
settings.agentBehavior.skills.sources.empty=구성된 스킬 소스가 없습니다.
|
||||||
|
settings.agentBehavior.skills.sources.title=추가 스킬 소스
|
||||||
|
settings.agentBehavior.skills.sources.add=추가
|
||||||
|
settings.agentBehavior.skills.sources.addPath=경로 추가
|
||||||
|
settings.agentBehavior.skills.sources.addPath.title=스킬 경로 추가
|
||||||
|
settings.agentBehavior.skills.sources.addPath.prompt=Kilo 스킬이 포함된 폴더를 선택하세요.
|
||||||
|
settings.agentBehavior.skills.sources.addUrl=URL 추가
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.title=스킬 소스 URL 추가
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.prompt=스킬 소스 URL을 입력하세요.
|
||||||
|
settings.agentBehavior.skills.sources.editPath.title=스킬 경로 편집
|
||||||
|
settings.agentBehavior.skills.sources.editUrl.title=스킬 URL 편집
|
||||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||||
revert.banner.count.one={0}개 메시지가 롤백됨
|
revert.banner.count.one={0}개 메시지가 롤백됨
|
||||||
@@ -376,3 +403,26 @@ revert.banner.redo=다시 실행
|
|||||||
revert.banner.redo.all=모두 다시 실행
|
revert.banner.redo.all=모두 다시 실행
|
||||||
revert.banner.hint=새 메시지를 보내기 전까지 이 변경 사항을 다시 실행할 수 있습니다
|
revert.banner.hint=새 메시지를 보내기 전까지 이 변경 사항을 다시 실행할 수 있습니다
|
||||||
revert.message.rollback=이 메시지로 롤백
|
revert.message.rollback=이 메시지로 롤백
|
||||||
|
|
||||||
|
# Context settings
|
||||||
|
settings.context.displayName=컨텍스트
|
||||||
|
settings.context.description=컨텍스트 동작을 구성합니다.
|
||||||
|
settings.context.save.pending=컨텍스트 설정 저장 중...
|
||||||
|
settings.context.save.failed=컨텍스트 설정을 저장하지 못했습니다
|
||||||
|
settings.context.compaction.title=압축
|
||||||
|
settings.context.compaction.auto.title=자동 압축
|
||||||
|
settings.context.compaction.auto.description=컨텍스트가 한도에 도달하기 전에 자동으로 압축
|
||||||
|
settings.context.compaction.threshold.title=자동 압축 한도
|
||||||
|
settings.context.compaction.threshold.description=컨텍스트가 모델 창의 이 비율에 도달하면 압축합니다. 안전 버퍼만 사용하려면 비워 두세요.
|
||||||
|
settings.context.compaction.threshold.placeholder=기본값
|
||||||
|
settings.context.compaction.threshold.suffix=%
|
||||||
|
settings.context.compaction.threshold.invalid=0에서 100 사이의 숫자를 입력하거나 필드를 비워 두세요.
|
||||||
|
settings.context.compaction.prune.title=이전 출력 정리
|
||||||
|
settings.context.compaction.prune.description=압축 중 이전 도구 출력 제거
|
||||||
|
settings.context.watcher.title=파일 감시자 무시 패턴
|
||||||
|
settings.context.watcher.description=감시자가 무시해야 할 파일의 글로브 패턴
|
||||||
|
settings.context.watcher.add=패턴 추가
|
||||||
|
settings.context.watcher.remove=선택한 패턴 제거
|
||||||
|
settings.context.watcher.empty=구성된 무시 패턴이 없습니다.
|
||||||
|
settings.context.watcher.input.title=무시 패턴 추가
|
||||||
|
settings.context.watcher.input.prompt=무시할 glob 패턴을 입력하세요:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
common.delete=Verwijderen
|
common.delete=Verwijderen
|
||||||
|
common.open=Openen
|
||||||
|
common.save=Opslaan
|
||||||
session.action.cancel=Annuleren
|
session.action.cancel=Annuleren
|
||||||
session.connection.connecting=Laden…
|
session.connection.connecting=Laden…
|
||||||
session.connection.error.app=Verbinding mislukt
|
session.connection.error.app=Verbinding mislukt
|
||||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
|||||||
|
|
||||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||||
settings.agentBehavior.mcp.addHint=Vraag de agent om een MCP-server toe te voegen.
|
settings.agentBehavior.mcp.addHint=Vraag de agent om een MCP-server toe te voegen.
|
||||||
|
settings.agentBehavior.skills.displayName=Vaardigheden
|
||||||
|
settings.agentBehavior.skills.search=Vaardigheden filteren
|
||||||
|
settings.agentBehavior.skills.empty=Geen vaardigheden gevonden.
|
||||||
|
settings.agentBehavior.skills.content.empty=Geen vaardigheidsinhoud beschikbaar.
|
||||||
|
settings.agentBehavior.skills.load.timeout=Het laden van vaardigheden is verlopen. Bestaande vaardigheden zijn behouden; verwijder trage of onbereikbare URL’s en vernieuw.
|
||||||
|
settings.agentBehavior.skills.reload.deferred=Vaardigheidsbron opgeslagen. Laad Core opnieuw nadat actieve sessies zijn voltooid om nieuwe vaardigheden toe te passen.
|
||||||
|
settings.agentBehavior.skills.reload.blocked=Vaardigheidsinstellingen opgeslagen, maar er zijn actieve sessies. Laad Core opnieuw nadat die sessies zijn voltooid om de nieuwe vaardigheden toe te passen.
|
||||||
|
settings.agentBehavior.skills.saved.notification=Vaardigheidsinstellingen opgeslagen
|
||||||
|
settings.agentBehavior.skills.delete.title=Vaardigheid verwijderen
|
||||||
|
settings.agentBehavior.skills.delete.message=Vaardigheid {0} verwijderen? Dit verwijdert het vaardigheidsbestand en kan niet ongedaan worden gemaakt.
|
||||||
|
settings.agentBehavior.skills.delete.failed=Kon de vaardigheid niet verwijderen.
|
||||||
|
settings.agentBehavior.skills.openInEditor=Openen in editor
|
||||||
|
settings.agentBehavior.skills.openInEditor.pending=Het vaardigheidsbestand wordt geopend nadat u Instellingen sluit.
|
||||||
|
settings.agentBehavior.skills.openInEditor.failed=Kon het vaardigheidsbestand niet openen in de editor.
|
||||||
|
settings.agentBehavior.skills.sources.empty=Geen vaardigheidsbronnen geconfigureerd.
|
||||||
|
settings.agentBehavior.skills.sources.title=Extra vaardigheidsbronnen
|
||||||
|
settings.agentBehavior.skills.sources.add=Toevoegen
|
||||||
|
settings.agentBehavior.skills.sources.addPath=Pad toevoegen
|
||||||
|
settings.agentBehavior.skills.sources.addPath.title=Vaardigheidspad toevoegen
|
||||||
|
settings.agentBehavior.skills.sources.addPath.prompt=Kies een map met Kilo-vaardigheden.
|
||||||
|
settings.agentBehavior.skills.sources.addUrl=URL toevoegen
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.title=URL van vaardigheidsbron toevoegen
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.prompt=Voer een URL van een vaardigheidsbron in.
|
||||||
|
settings.agentBehavior.skills.sources.editPath.title=Vaardigheidspad bewerken
|
||||||
|
settings.agentBehavior.skills.sources.editUrl.title=Vaardigheids-URL bewerken
|
||||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||||
revert.banner.count.one={0} bericht teruggedraaid
|
revert.banner.count.one={0} bericht teruggedraaid
|
||||||
@@ -376,3 +403,26 @@ revert.banner.redo=Opnieuw uitvoeren
|
|||||||
revert.banner.redo.all=Alles opnieuw uitvoeren
|
revert.banner.redo.all=Alles opnieuw uitvoeren
|
||||||
revert.banner.hint=Je kunt deze wijzigingen opnieuw uitvoeren totdat je een nieuw bericht verstuurt
|
revert.banner.hint=Je kunt deze wijzigingen opnieuw uitvoeren totdat je een nieuw bericht verstuurt
|
||||||
revert.message.rollback=Terugdraaien naar dit bericht
|
revert.message.rollback=Terugdraaien naar dit bericht
|
||||||
|
|
||||||
|
# Context settings
|
||||||
|
settings.context.displayName=Context
|
||||||
|
settings.context.description=Contextgedrag configureren.
|
||||||
|
settings.context.save.pending=Contextinstellingen opslaan...
|
||||||
|
settings.context.save.failed=Contextinstellingen opslaan mislukt
|
||||||
|
settings.context.compaction.title=Compactie
|
||||||
|
settings.context.compaction.auto.title=Automatische Compactie
|
||||||
|
settings.context.compaction.auto.description=Context automatisch compacteren voordat deze de limiet bereikt
|
||||||
|
settings.context.compaction.threshold.title=Limiet voor automatisch compacteren
|
||||||
|
settings.context.compaction.threshold.description=Compacteer wanneer de context dit percentage van het modelvenster bereikt. Laat leeg om alleen de veiligheidsbuffer te gebruiken.
|
||||||
|
settings.context.compaction.threshold.placeholder=Standaard
|
||||||
|
settings.context.compaction.threshold.suffix=%
|
||||||
|
settings.context.compaction.threshold.invalid=Voer een getal van 0 tot 100 in, of laat het veld leeg.
|
||||||
|
settings.context.compaction.prune.title=Oude Uitvoer Opschonen
|
||||||
|
settings.context.compaction.prune.description=Verwijder oude tool uitvoer tijdens compactie
|
||||||
|
settings.context.watcher.title=File Watcher Negeer Patronen
|
||||||
|
settings.context.watcher.description=Glob-patronen voor bestanden die de watcher moet negeren
|
||||||
|
settings.context.watcher.add=Patroon toevoegen
|
||||||
|
settings.context.watcher.remove=Geselecteerde patronen verwijderen
|
||||||
|
settings.context.watcher.empty=Geen negeerpatronen geconfigureerd.
|
||||||
|
settings.context.watcher.input.title=Negeerpatroon toevoegen
|
||||||
|
settings.context.watcher.input.prompt=Voer een glob-patroon in om te negeren:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
common.delete=Slett
|
common.delete=Slett
|
||||||
|
common.open=Åpne
|
||||||
|
common.save=Lagre
|
||||||
session.action.cancel=Avbryt
|
session.action.cancel=Avbryt
|
||||||
session.connection.connecting=Laster…
|
session.connection.connecting=Laster…
|
||||||
session.connection.error.app=Tilkoblingsfeil
|
session.connection.error.app=Tilkoblingsfeil
|
||||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
|||||||
|
|
||||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||||
settings.agentBehavior.mcp.addHint=Be agenten om å legge til en MCP-server.
|
settings.agentBehavior.mcp.addHint=Be agenten om å legge til en MCP-server.
|
||||||
|
settings.agentBehavior.skills.displayName=Ferdigheter
|
||||||
|
settings.agentBehavior.skills.search=Filtrer ferdigheter
|
||||||
|
settings.agentBehavior.skills.empty=Ingen ferdigheter funnet.
|
||||||
|
settings.agentBehavior.skills.content.empty=Ingen ferdighetsinnhold tilgjengelig.
|
||||||
|
settings.agentBehavior.skills.load.timeout=Innlasting av ferdigheter tidsavbrutt. Eksisterende ferdigheter ble beholdt; fjern trege eller utilgjengelige URL-er og oppdater.
|
||||||
|
settings.agentBehavior.skills.reload.deferred=Ferdighetskilden ble lagret. Last Core på nytt etter at aktive økter er ferdige for å bruke nye ferdigheter.
|
||||||
|
settings.agentBehavior.skills.reload.blocked=Ferdighetsinnstillinger ble lagret, men det finnes aktive økter. Last Core på nytt etter at disse øktene er ferdige for å bruke de nye ferdighetene.
|
||||||
|
settings.agentBehavior.skills.saved.notification=Ferdighetsinnstillinger lagret
|
||||||
|
settings.agentBehavior.skills.delete.title=Slett ferdighet
|
||||||
|
settings.agentBehavior.skills.delete.message=Slette ferdigheten {0}? Dette fjerner ferdighetsfilen og kan ikke angres.
|
||||||
|
settings.agentBehavior.skills.delete.failed=Kunne ikke slette ferdigheten.
|
||||||
|
settings.agentBehavior.skills.openInEditor=Åpne i editor
|
||||||
|
settings.agentBehavior.skills.openInEditor.pending=Ferdighetsfilen åpnes etter at du lukker Innstillinger.
|
||||||
|
settings.agentBehavior.skills.openInEditor.failed=Kunne ikke åpne ferdighetsfilen i editoren.
|
||||||
|
settings.agentBehavior.skills.sources.empty=Ingen ferdighetskilder konfigurert.
|
||||||
|
settings.agentBehavior.skills.sources.title=Flere ferdighetskilder
|
||||||
|
settings.agentBehavior.skills.sources.add=Legg til
|
||||||
|
settings.agentBehavior.skills.sources.addPath=Legg til sti
|
||||||
|
settings.agentBehavior.skills.sources.addPath.title=Legg til ferdighetssti
|
||||||
|
settings.agentBehavior.skills.sources.addPath.prompt=Velg en mappe som inneholder Kilo-ferdigheter.
|
||||||
|
settings.agentBehavior.skills.sources.addUrl=Legg til URL
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.title=Legg til URL for ferdighetskilde
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.prompt=Skriv inn en URL for ferdighetskilde.
|
||||||
|
settings.agentBehavior.skills.sources.editPath.title=Rediger ferdighetssti
|
||||||
|
settings.agentBehavior.skills.sources.editUrl.title=Rediger ferdighets-URL
|
||||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||||
revert.banner.count.one={0} melding rullet tilbake
|
revert.banner.count.one={0} melding rullet tilbake
|
||||||
@@ -376,3 +403,26 @@ revert.banner.redo=Gjør om
|
|||||||
revert.banner.redo.all=Gjør om alle
|
revert.banner.redo.all=Gjør om alle
|
||||||
revert.banner.hint=Du kan gjøre om disse endringene til du sender en ny melding
|
revert.banner.hint=Du kan gjøre om disse endringene til du sender en ny melding
|
||||||
revert.message.rollback=Rull tilbake til denne meldingen
|
revert.message.rollback=Rull tilbake til denne meldingen
|
||||||
|
|
||||||
|
# Context settings
|
||||||
|
settings.context.displayName=Kontekst
|
||||||
|
settings.context.description=Konfigurer kontekstadferd.
|
||||||
|
settings.context.save.pending=Lagrer kontekstinnstillinger...
|
||||||
|
settings.context.save.failed=Kunne ikke lagre kontekstinnstillinger
|
||||||
|
settings.context.compaction.title=Komprimering
|
||||||
|
settings.context.compaction.auto.title=Automatisk komprimering
|
||||||
|
settings.context.compaction.auto.description=Komprimer automatisk kontekst før den når grensen
|
||||||
|
settings.context.compaction.threshold.title=Grense for automatisk komprimering
|
||||||
|
settings.context.compaction.threshold.description=Komprimer når konteksten når denne prosentandelen av modellvinduet. La stå tomt for å bare bruke sikkerhetsbufferen.
|
||||||
|
settings.context.compaction.threshold.placeholder=Standard
|
||||||
|
settings.context.compaction.threshold.suffix=%
|
||||||
|
settings.context.compaction.threshold.invalid=Skriv inn et tall fra 0 til 100, eller la feltet stå tomt.
|
||||||
|
settings.context.compaction.prune.title=Fjern gamle utdata
|
||||||
|
settings.context.compaction.prune.description=Fjern gamle verktøyutdata under komprimering
|
||||||
|
settings.context.watcher.title=Filvakt-ignormønstre
|
||||||
|
settings.context.watcher.description=Glob-mønstre for filer som vakten skal ignorere
|
||||||
|
settings.context.watcher.add=Legg til mønster
|
||||||
|
settings.context.watcher.remove=Fjern valgte mønstre
|
||||||
|
settings.context.watcher.empty=Ingen ignormønstre konfigurert.
|
||||||
|
settings.context.watcher.input.title=Legg til ignormønster
|
||||||
|
settings.context.watcher.input.prompt=Skriv inn et glob-mønster som skal ignoreres:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
common.delete=Usuń
|
common.delete=Usuń
|
||||||
|
common.open=Otwórz
|
||||||
|
common.save=Zapisz
|
||||||
session.action.cancel=Anuluj
|
session.action.cancel=Anuluj
|
||||||
session.connection.connecting=Ładowanie…
|
session.connection.connecting=Ładowanie…
|
||||||
session.connection.error.app=Błąd połączenia
|
session.connection.error.app=Błąd połączenia
|
||||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
|||||||
|
|
||||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||||
settings.agentBehavior.mcp.addHint=Aby dodać serwer MCP, poproś agenta, aby to zrobił.
|
settings.agentBehavior.mcp.addHint=Aby dodać serwer MCP, poproś agenta, aby to zrobił.
|
||||||
|
settings.agentBehavior.skills.displayName=Umiejętności
|
||||||
|
settings.agentBehavior.skills.search=Filtruj umiejętności
|
||||||
|
settings.agentBehavior.skills.empty=Nie znaleziono umiejętności.
|
||||||
|
settings.agentBehavior.skills.content.empty=Brak dostępnej treści umiejętności.
|
||||||
|
settings.agentBehavior.skills.load.timeout=Przekroczono czas ładowania umiejętności. Istniejące umiejętności zostały zachowane; usuń wolne lub niedostępne adresy URL i odśwież.
|
||||||
|
settings.agentBehavior.skills.reload.deferred=Źródło umiejętności zapisane. Przeładuj Core po zakończeniu aktywnych sesji, aby zastosować nowe umiejętności.
|
||||||
|
settings.agentBehavior.skills.reload.blocked=Ustawienia umiejętności zapisane, ale są aktywne sesje. Przeładuj Core po zakończeniu tych sesji, aby zastosować nowe umiejętności.
|
||||||
|
settings.agentBehavior.skills.saved.notification=Ustawienia umiejętności zapisane
|
||||||
|
settings.agentBehavior.skills.delete.title=Usuń umiejętność
|
||||||
|
settings.agentBehavior.skills.delete.message=Usunąć umiejętność {0}? Spowoduje to usunięcie pliku umiejętności i nie można tego cofnąć.
|
||||||
|
settings.agentBehavior.skills.delete.failed=Nie można usunąć umiejętności.
|
||||||
|
settings.agentBehavior.skills.openInEditor=Otwórz w edytorze
|
||||||
|
settings.agentBehavior.skills.openInEditor.pending=Plik umiejętności otworzy się po zamknięciu Ustawień.
|
||||||
|
settings.agentBehavior.skills.openInEditor.failed=Nie można otworzyć pliku umiejętności w edytorze.
|
||||||
|
settings.agentBehavior.skills.sources.empty=Nie skonfigurowano źródeł umiejętności.
|
||||||
|
settings.agentBehavior.skills.sources.title=Dodatkowe źródła umiejętności
|
||||||
|
settings.agentBehavior.skills.sources.add=Dodaj
|
||||||
|
settings.agentBehavior.skills.sources.addPath=Dodaj ścieżkę
|
||||||
|
settings.agentBehavior.skills.sources.addPath.title=Dodaj ścieżkę umiejętności
|
||||||
|
settings.agentBehavior.skills.sources.addPath.prompt=Wybierz folder zawierający umiejętności Kilo.
|
||||||
|
settings.agentBehavior.skills.sources.addUrl=Dodaj URL
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.title=Dodaj URL źródła umiejętności
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.prompt=Wpisz URL źródła umiejętności.
|
||||||
|
settings.agentBehavior.skills.sources.editPath.title=Edytuj ścieżkę umiejętności
|
||||||
|
settings.agentBehavior.skills.sources.editUrl.title=Edytuj URL umiejętności
|
||||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||||
revert.banner.count.one=Cofnięto {0} wiadomość
|
revert.banner.count.one=Cofnięto {0} wiadomość
|
||||||
@@ -376,3 +403,26 @@ revert.banner.redo=Ponów
|
|||||||
revert.banner.redo.all=Ponów wszystko
|
revert.banner.redo.all=Ponów wszystko
|
||||||
revert.banner.hint=Możesz ponowić te zmiany, dopóki nie wyślesz nowej wiadomości
|
revert.banner.hint=Możesz ponowić te zmiany, dopóki nie wyślesz nowej wiadomości
|
||||||
revert.message.rollback=Cofnij do tej wiadomości
|
revert.message.rollback=Cofnij do tej wiadomości
|
||||||
|
|
||||||
|
# Context settings
|
||||||
|
settings.context.displayName=Kontekst
|
||||||
|
settings.context.description=Skonfiguruj zachowanie kontekstu.
|
||||||
|
settings.context.save.pending=Zapisywanie ustawień kontekstu...
|
||||||
|
settings.context.save.failed=Nie udało się zapisać ustawień kontekstu
|
||||||
|
settings.context.compaction.title=Kompaktowanie
|
||||||
|
settings.context.compaction.auto.title=Automatyczna kompakcja
|
||||||
|
settings.context.compaction.auto.description=Automatycznie kompaktuj kontekst, zanim osiągnie limit
|
||||||
|
settings.context.compaction.threshold.title=Limit automatycznego kompaktowania
|
||||||
|
settings.context.compaction.threshold.description=Kompaktuj, gdy kontekst osiągnie ten procent okna modelu. Pozostaw puste, aby używać tylko bufora bezpieczeństwa.
|
||||||
|
settings.context.compaction.threshold.placeholder=Domyślne
|
||||||
|
settings.context.compaction.threshold.suffix=%
|
||||||
|
settings.context.compaction.threshold.invalid=Wpisz liczbę od 0 do 100 albo pozostaw pole puste.
|
||||||
|
settings.context.compaction.prune.title=Przytnij stare wyjścia
|
||||||
|
settings.context.compaction.prune.description=Usuń stare wyjścia narzędzi podczas kompakcji
|
||||||
|
settings.context.watcher.title=Wzorce ignorowania obserwatora plików
|
||||||
|
settings.context.watcher.description=Wzorce glob dla plików do ignorowania
|
||||||
|
settings.context.watcher.add=Dodaj wzorzec
|
||||||
|
settings.context.watcher.remove=Usuń wybrane wzorce
|
||||||
|
settings.context.watcher.empty=Nie skonfigurowano wzorców ignorowania.
|
||||||
|
settings.context.watcher.input.title=Dodaj wzorzec ignorowania
|
||||||
|
settings.context.watcher.input.prompt=Wpisz wzorzec glob do ignorowania:
|
||||||
|
|||||||
+50
@@ -1,4 +1,6 @@
|
|||||||
common.delete=Excluir
|
common.delete=Excluir
|
||||||
|
common.open=Abrir
|
||||||
|
common.save=Salvar
|
||||||
session.action.cancel=Cancelar
|
session.action.cancel=Cancelar
|
||||||
session.connection.connecting=Carregando…
|
session.connection.connecting=Carregando…
|
||||||
session.connection.error.app=Falha na conexão
|
session.connection.error.app=Falha na conexão
|
||||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
|||||||
|
|
||||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||||
settings.agentBehavior.mcp.addHint=Para adicionar um servidor MCP, peça ao agente para fazer isso.
|
settings.agentBehavior.mcp.addHint=Para adicionar um servidor MCP, peça ao agente para fazer isso.
|
||||||
|
settings.agentBehavior.skills.displayName=Habilidades
|
||||||
|
settings.agentBehavior.skills.search=Filtrar habilidades
|
||||||
|
settings.agentBehavior.skills.empty=Nenhuma habilidade encontrada.
|
||||||
|
settings.agentBehavior.skills.content.empty=Nenhum conteúdo de habilidade disponível.
|
||||||
|
settings.agentBehavior.skills.load.timeout=O carregamento de habilidades atingiu o tempo limite. As habilidades existentes foram mantidas; remova URLs lentas ou inacessíveis e atualize.
|
||||||
|
settings.agentBehavior.skills.reload.deferred=Fonte de habilidades salva. Recarregue o Core depois que as sessões ativas terminarem para aplicar novas habilidades.
|
||||||
|
settings.agentBehavior.skills.reload.blocked=Configurações de habilidades salvas, mas há sessões ativas. Recarregue o Core depois que essas sessões terminarem para aplicar as novas habilidades.
|
||||||
|
settings.agentBehavior.skills.saved.notification=Configurações de habilidades salvas
|
||||||
|
settings.agentBehavior.skills.delete.title=Excluir habilidade
|
||||||
|
settings.agentBehavior.skills.delete.message=Excluir a habilidade {0}? Isso remove o arquivo da habilidade e não pode ser desfeito.
|
||||||
|
settings.agentBehavior.skills.delete.failed=Não foi possível excluir a habilidade.
|
||||||
|
settings.agentBehavior.skills.openInEditor=Abrir no editor
|
||||||
|
settings.agentBehavior.skills.openInEditor.pending=O arquivo da habilidade será aberto depois que você fechar as Configurações.
|
||||||
|
settings.agentBehavior.skills.openInEditor.failed=Não foi possível abrir o arquivo da habilidade no editor.
|
||||||
|
settings.agentBehavior.skills.sources.empty=Nenhuma fonte de habilidades configurada.
|
||||||
|
settings.agentBehavior.skills.sources.title=Fontes de habilidades adicionais
|
||||||
|
settings.agentBehavior.skills.sources.add=Adicionar
|
||||||
|
settings.agentBehavior.skills.sources.addPath=Adicionar caminho
|
||||||
|
settings.agentBehavior.skills.sources.addPath.title=Adicionar caminho de habilidades
|
||||||
|
settings.agentBehavior.skills.sources.addPath.prompt=Escolha uma pasta contendo habilidades do Kilo.
|
||||||
|
settings.agentBehavior.skills.sources.addUrl=Adicionar URL
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.title=Adicionar URL de fonte de habilidades
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.prompt=Insira uma URL de fonte de habilidades.
|
||||||
|
settings.agentBehavior.skills.sources.editPath.title=Editar caminho de habilidades
|
||||||
|
settings.agentBehavior.skills.sources.editUrl.title=Editar URL de habilidades
|
||||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||||
revert.banner.count.one={0} mensagem revertida
|
revert.banner.count.one={0} mensagem revertida
|
||||||
@@ -376,3 +403,26 @@ revert.banner.redo=Refazer
|
|||||||
revert.banner.redo.all=Refazer tudo
|
revert.banner.redo.all=Refazer tudo
|
||||||
revert.banner.hint=Você pode refazer essas alterações até enviar uma nova mensagem
|
revert.banner.hint=Você pode refazer essas alterações até enviar uma nova mensagem
|
||||||
revert.message.rollback=Reverter para esta mensagem
|
revert.message.rollback=Reverter para esta mensagem
|
||||||
|
|
||||||
|
# Context settings
|
||||||
|
settings.context.displayName=Contexto
|
||||||
|
settings.context.description=Configure o comportamento do contexto.
|
||||||
|
settings.context.save.pending=Salvando configurações de contexto...
|
||||||
|
settings.context.save.failed=Falha ao salvar configurações de contexto
|
||||||
|
settings.context.compaction.title=Compactação
|
||||||
|
settings.context.compaction.auto.title=Compactação automática
|
||||||
|
settings.context.compaction.auto.description=Compactar automaticamente o contexto antes que atinja o limite
|
||||||
|
settings.context.compaction.threshold.title=Limite de compactação automática
|
||||||
|
settings.context.compaction.threshold.description=Compacte quando o contexto atingir esta porcentagem da janela do modelo. Deixe em branco para usar apenas a margem de segurança.
|
||||||
|
settings.context.compaction.threshold.placeholder=Padrão
|
||||||
|
settings.context.compaction.threshold.suffix=%
|
||||||
|
settings.context.compaction.threshold.invalid=Digite um número de 0 a 100 ou deixe o campo em branco.
|
||||||
|
settings.context.compaction.prune.title=Remover saídas antigas
|
||||||
|
settings.context.compaction.prune.description=Remover saídas antigas de ferramentas durante a compactação
|
||||||
|
settings.context.watcher.title=Padrões de ignorar do observador
|
||||||
|
settings.context.watcher.description=Padrões glob para arquivos que o observador deve ignorar
|
||||||
|
settings.context.watcher.add=Adicionar padrão
|
||||||
|
settings.context.watcher.remove=Remover padrões selecionados
|
||||||
|
settings.context.watcher.empty=Nenhum padrão de ignorar configurado.
|
||||||
|
settings.context.watcher.input.title=Adicionar padrão de ignorar
|
||||||
|
settings.context.watcher.input.prompt=Digite um padrão glob para ignorar:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
common.delete=Удалить
|
common.delete=Удалить
|
||||||
|
common.open=Открыть
|
||||||
|
common.save=Сохранить
|
||||||
session.action.cancel=Отмена
|
session.action.cancel=Отмена
|
||||||
session.connection.connecting=Загрузка…
|
session.connection.connecting=Загрузка…
|
||||||
session.connection.error.app=Ошибка подключения
|
session.connection.error.app=Ошибка подключения
|
||||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
|||||||
|
|
||||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||||
settings.agentBehavior.mcp.addHint=Чтобы добавить MCP-сервер, попросите агента сделать это.
|
settings.agentBehavior.mcp.addHint=Чтобы добавить MCP-сервер, попросите агента сделать это.
|
||||||
|
settings.agentBehavior.skills.displayName=Навыки
|
||||||
|
settings.agentBehavior.skills.search=Фильтр навыков
|
||||||
|
settings.agentBehavior.skills.empty=Навыки не найдены.
|
||||||
|
settings.agentBehavior.skills.content.empty=Нет доступного содержимого навыка.
|
||||||
|
settings.agentBehavior.skills.load.timeout=Время загрузки навыков истекло. Существующие навыки сохранены; удалите медленные или недоступные URL и обновите.
|
||||||
|
settings.agentBehavior.skills.reload.deferred=Источник навыков сохранён. Перезагрузите Core после завершения активных сессий, чтобы применить новые навыки.
|
||||||
|
settings.agentBehavior.skills.reload.blocked=Настройки навыков сохранены, но есть активные сессии. Перезагрузите Core после завершения этих сессий, чтобы применить новые навыки.
|
||||||
|
settings.agentBehavior.skills.saved.notification=Настройки навыков сохранены
|
||||||
|
settings.agentBehavior.skills.delete.title=Удалить навык
|
||||||
|
settings.agentBehavior.skills.delete.message=Удалить навык {0}? Это удалит файл навыка, и действие нельзя будет отменить.
|
||||||
|
settings.agentBehavior.skills.delete.failed=Не удалось удалить навык.
|
||||||
|
settings.agentBehavior.skills.openInEditor=Открыть в редакторе
|
||||||
|
settings.agentBehavior.skills.openInEditor.pending=Файл навыка откроется после закрытия настроек.
|
||||||
|
settings.agentBehavior.skills.openInEditor.failed=Не удалось открыть файл навыка в редакторе.
|
||||||
|
settings.agentBehavior.skills.sources.empty=Источники навыков не настроены.
|
||||||
|
settings.agentBehavior.skills.sources.title=Дополнительные источники навыков
|
||||||
|
settings.agentBehavior.skills.sources.add=Добавить
|
||||||
|
settings.agentBehavior.skills.sources.addPath=Добавить путь
|
||||||
|
settings.agentBehavior.skills.sources.addPath.title=Добавить путь к навыкам
|
||||||
|
settings.agentBehavior.skills.sources.addPath.prompt=Выберите папку, содержащую навыки Kilo.
|
||||||
|
settings.agentBehavior.skills.sources.addUrl=Добавить URL
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.title=Добавить URL источника навыков
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.prompt=Введите URL источника навыков.
|
||||||
|
settings.agentBehavior.skills.sources.editPath.title=Изменить путь к навыкам
|
||||||
|
settings.agentBehavior.skills.sources.editUrl.title=Изменить URL навыков
|
||||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||||
revert.banner.count.one=Отменено сообщений: {0}
|
revert.banner.count.one=Отменено сообщений: {0}
|
||||||
@@ -376,3 +403,26 @@ revert.banner.redo=Повторить
|
|||||||
revert.banner.redo.all=Повторить все
|
revert.banner.redo.all=Повторить все
|
||||||
revert.banner.hint=Эти изменения можно повторить до отправки нового сообщения
|
revert.banner.hint=Эти изменения можно повторить до отправки нового сообщения
|
||||||
revert.message.rollback=Откатиться к этому сообщению
|
revert.message.rollback=Откатиться к этому сообщению
|
||||||
|
|
||||||
|
# Context settings
|
||||||
|
settings.context.displayName=Контекст
|
||||||
|
settings.context.description=Настройте поведение контекста.
|
||||||
|
settings.context.save.pending=Сохранение настроек контекста...
|
||||||
|
settings.context.save.failed=Не удалось сохранить настройки контекста
|
||||||
|
settings.context.compaction.title=Сжатие
|
||||||
|
settings.context.compaction.auto.title=Автоматическое сжатие
|
||||||
|
settings.context.compaction.auto.description=Автоматически сжимать контекст до достижения лимита
|
||||||
|
settings.context.compaction.threshold.title=Лимит автоматического сжатия
|
||||||
|
settings.context.compaction.threshold.description=Сжимать, когда контекст достигает этого процента окна модели. Оставьте пустым, чтобы использовать только буфер безопасности.
|
||||||
|
settings.context.compaction.threshold.placeholder=По умолчанию
|
||||||
|
settings.context.compaction.threshold.suffix=%
|
||||||
|
settings.context.compaction.threshold.invalid=Введите число от 0 до 100 или оставьте поле пустым.
|
||||||
|
settings.context.compaction.prune.title=Очистить старые выходные данные
|
||||||
|
settings.context.compaction.prune.description=Удалить старые выходные данные инструментов при сжатии
|
||||||
|
settings.context.watcher.title=Шаблоны игнорирования наблюдателя файлов
|
||||||
|
settings.context.watcher.description=Glob-шаблоны для файлов, которые наблюдатель должен игнорировать
|
||||||
|
settings.context.watcher.add=Добавить шаблон
|
||||||
|
settings.context.watcher.remove=Удалить выбранные шаблоны
|
||||||
|
settings.context.watcher.empty=Шаблоны игнорирования не настроены.
|
||||||
|
settings.context.watcher.input.title=Добавить шаблон игнорирования
|
||||||
|
settings.context.watcher.input.prompt=Введите glob-шаблон для игнорирования:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
common.delete=ลบ
|
common.delete=ลบ
|
||||||
|
common.open=เปิด
|
||||||
|
common.save=บันทึก
|
||||||
session.action.cancel=ยกเลิก
|
session.action.cancel=ยกเลิก
|
||||||
session.connection.connecting=กำลังโหลด…
|
session.connection.connecting=กำลังโหลด…
|
||||||
session.connection.error.app=การเชื่อมต่อล้มเหลว
|
session.connection.error.app=การเชื่อมต่อล้มเหลว
|
||||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
|||||||
|
|
||||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||||
settings.agentBehavior.mcp.addHint=หากต้องการเพิ่มเซิร์ฟเวอร์ MCP ให้ขอให้เอเจนต์เพิ่มให้
|
settings.agentBehavior.mcp.addHint=หากต้องการเพิ่มเซิร์ฟเวอร์ MCP ให้ขอให้เอเจนต์เพิ่มให้
|
||||||
|
settings.agentBehavior.skills.displayName=ทักษะ
|
||||||
|
settings.agentBehavior.skills.search=กรองทักษะ
|
||||||
|
settings.agentBehavior.skills.empty=ไม่พบทักษะ
|
||||||
|
settings.agentBehavior.skills.content.empty=ไม่มีเนื้อหาทักษะที่พร้อมใช้งาน
|
||||||
|
settings.agentBehavior.skills.load.timeout=การโหลดทักษะหมดเวลา ระบบเก็บทักษะเดิมไว้แล้ว โปรดลบ URL ที่ช้าหรือเข้าถึงไม่ได้แล้วรีเฟรช
|
||||||
|
settings.agentBehavior.skills.reload.deferred=บันทึกแหล่งที่มาทักษะแล้ว โหลด Core ใหม่หลังจากเซสชันที่ใช้งานอยู่สิ้นสุดเพื่อใช้ทักษะใหม่
|
||||||
|
settings.agentBehavior.skills.reload.blocked=บันทึกการตั้งค่าทักษะแล้ว แต่ยังมีเซสชันที่ใช้งานอยู่ โหลด Core ใหม่หลังจากเซสชันเหล่านั้นสิ้นสุดเพื่อใช้ทักษะใหม่
|
||||||
|
settings.agentBehavior.skills.saved.notification=บันทึกการตั้งค่าทักษะแล้ว
|
||||||
|
settings.agentBehavior.skills.delete.title=ลบทักษะ
|
||||||
|
settings.agentBehavior.skills.delete.message=ลบทักษะ {0} หรือไม่ การดำเนินการนี้จะลบไฟล์ทักษะและไม่สามารถย้อนกลับได้
|
||||||
|
settings.agentBehavior.skills.delete.failed=ไม่สามารถลบทักษะได้
|
||||||
|
settings.agentBehavior.skills.openInEditor=เปิดในตัวแก้ไข
|
||||||
|
settings.agentBehavior.skills.openInEditor.pending=ไฟล์ทักษะจะเปิดหลังจากคุณปิดการตั้งค่า
|
||||||
|
settings.agentBehavior.skills.openInEditor.failed=ไม่สามารถเปิดไฟล์ทักษะในตัวแก้ไขได้
|
||||||
|
settings.agentBehavior.skills.sources.empty=ไม่ได้กำหนดค่าแหล่งที่มาทักษะ
|
||||||
|
settings.agentBehavior.skills.sources.title=แหล่งที่มาทักษะเพิ่มเติม
|
||||||
|
settings.agentBehavior.skills.sources.add=เพิ่ม
|
||||||
|
settings.agentBehavior.skills.sources.addPath=เพิ่มเส้นทาง
|
||||||
|
settings.agentBehavior.skills.sources.addPath.title=เพิ่มเส้นทางทักษะ
|
||||||
|
settings.agentBehavior.skills.sources.addPath.prompt=เลือกโฟลเดอร์ที่มีทักษะ Kilo
|
||||||
|
settings.agentBehavior.skills.sources.addUrl=เพิ่ม URL
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.title=เพิ่ม URL แหล่งที่มาทักษะ
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.prompt=ป้อน URL แหล่งที่มาทักษะ
|
||||||
|
settings.agentBehavior.skills.sources.editPath.title=แก้ไขเส้นทางทักษะ
|
||||||
|
settings.agentBehavior.skills.sources.editUrl.title=แก้ไข URL ทักษะ
|
||||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||||
revert.banner.count.one=ย้อนกลับข้อความ {0} รายการแล้ว
|
revert.banner.count.one=ย้อนกลับข้อความ {0} รายการแล้ว
|
||||||
@@ -376,3 +403,26 @@ revert.banner.redo=ทำซ้ำ
|
|||||||
revert.banner.redo.all=ทำซ้ำทั้งหมด
|
revert.banner.redo.all=ทำซ้ำทั้งหมด
|
||||||
revert.banner.hint=คุณสามารถทำซ้ำการเปลี่ยนแปลงเหล่านี้ได้จนกว่าจะส่งข้อความใหม่
|
revert.banner.hint=คุณสามารถทำซ้ำการเปลี่ยนแปลงเหล่านี้ได้จนกว่าจะส่งข้อความใหม่
|
||||||
revert.message.rollback=ย้อนกลับไปยังข้อความนี้
|
revert.message.rollback=ย้อนกลับไปยังข้อความนี้
|
||||||
|
|
||||||
|
# Context settings
|
||||||
|
settings.context.displayName=บริบท
|
||||||
|
settings.context.description=กำหนดค่าพฤติกรรมของบริบท
|
||||||
|
settings.context.save.pending=กำลังบันทึกการตั้งค่าบริบท...
|
||||||
|
settings.context.save.failed=บันทึกการตั้งค่าบริบทไม่สำเร็จ
|
||||||
|
settings.context.compaction.title=การบีบอัด
|
||||||
|
settings.context.compaction.auto.title=การบีบอัดอัตโนมัติ
|
||||||
|
settings.context.compaction.auto.description=บีบอัดบริบทอัตโนมัติก่อนถึงขีดจำกัด
|
||||||
|
settings.context.compaction.threshold.title=ขีดจำกัดการบีบอัดอัตโนมัติ
|
||||||
|
settings.context.compaction.threshold.description=บีบอัดเมื่อบริบทถึงเปอร์เซ็นต์นี้ของหน้าต่างโมเดล เว้นว่างไว้เพื่อใช้เฉพาะบัฟเฟอร์ความปลอดภัย
|
||||||
|
settings.context.compaction.threshold.placeholder=ค่าเริ่มต้น
|
||||||
|
settings.context.compaction.threshold.suffix=%
|
||||||
|
settings.context.compaction.threshold.invalid=ป้อนตัวเลขตั้งแต่ 0 ถึง 100 หรือเว้นช่องว่างไว้
|
||||||
|
settings.context.compaction.prune.title=ตัดผลลัพธ์เก่า
|
||||||
|
settings.context.compaction.prune.description=ลบผลลัพธ์เครื่องมือเก่าระหว่างการบีบอัด
|
||||||
|
settings.context.watcher.title=รูปแบบการละเว้นตัวเฝ้าดูไฟล์
|
||||||
|
settings.context.watcher.description=รูปแบบ glob สำหรับไฟล์ที่ตัวเฝ้าดูควรละเว้น
|
||||||
|
settings.context.watcher.add=เพิ่มรูปแบบ
|
||||||
|
settings.context.watcher.remove=ลบรูปแบบที่เลือก
|
||||||
|
settings.context.watcher.empty=ยังไม่ได้กำหนดค่ารูปแบบการละเว้น
|
||||||
|
settings.context.watcher.input.title=เพิ่มรูปแบบการละเว้น
|
||||||
|
settings.context.watcher.input.prompt=ป้อนรูปแบบ glob ที่จะละเว้น:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
common.delete=Sil
|
common.delete=Sil
|
||||||
|
common.open=Aç
|
||||||
|
common.save=Kaydet
|
||||||
session.action.cancel=İptal
|
session.action.cancel=İptal
|
||||||
session.connection.connecting=Yükleniyor…
|
session.connection.connecting=Yükleniyor…
|
||||||
session.connection.error.app=Bağlantı hatası
|
session.connection.error.app=Bağlantı hatası
|
||||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
|||||||
|
|
||||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||||
settings.agentBehavior.mcp.addHint=MCP sunucusu eklemek için ajandan bunu yapmasını isteyin.
|
settings.agentBehavior.mcp.addHint=MCP sunucusu eklemek için ajandan bunu yapmasını isteyin.
|
||||||
|
settings.agentBehavior.skills.displayName=Beceriler
|
||||||
|
settings.agentBehavior.skills.search=Becerileri filtrele
|
||||||
|
settings.agentBehavior.skills.empty=Beceri bulunamadı.
|
||||||
|
settings.agentBehavior.skills.content.empty=Kullanılabilir beceri içeriği yok.
|
||||||
|
settings.agentBehavior.skills.load.timeout=Beceriler yüklenirken zaman aşımına uğradı. Mevcut beceriler korundu; yavaş veya ulaşılamayan URL'leri kaldırıp yenileyin.
|
||||||
|
settings.agentBehavior.skills.reload.deferred=Beceri kaynağı kaydedildi. Yeni becerileri uygulamak için etkin oturumlar bittikten sonra Core'u yeniden yükleyin.
|
||||||
|
settings.agentBehavior.skills.reload.blocked=Beceri ayarları kaydedildi, ancak etkin oturumlar var. Yeni becerileri uygulamak için bu oturumlar bittikten sonra Core'u yeniden yükleyin.
|
||||||
|
settings.agentBehavior.skills.saved.notification=Beceri ayarları kaydedildi
|
||||||
|
settings.agentBehavior.skills.delete.title=Beceriyi Sil
|
||||||
|
settings.agentBehavior.skills.delete.message={0} becerisi silinsin mi? Bu işlem beceri dosyasını kaldırır ve geri alınamaz.
|
||||||
|
settings.agentBehavior.skills.delete.failed=Beceri silinemedi.
|
||||||
|
settings.agentBehavior.skills.openInEditor=Düzenleyicide Aç
|
||||||
|
settings.agentBehavior.skills.openInEditor.pending=Beceri dosyası Ayarlar kapatıldıktan sonra açılacak.
|
||||||
|
settings.agentBehavior.skills.openInEditor.failed=Beceri dosyası düzenleyicide açılamadı.
|
||||||
|
settings.agentBehavior.skills.sources.empty=Yapılandırılmış beceri kaynağı yok.
|
||||||
|
settings.agentBehavior.skills.sources.title=Ek Beceri Kaynakları
|
||||||
|
settings.agentBehavior.skills.sources.add=Ekle
|
||||||
|
settings.agentBehavior.skills.sources.addPath=Yol ekle
|
||||||
|
settings.agentBehavior.skills.sources.addPath.title=Beceri Yolu Ekle
|
||||||
|
settings.agentBehavior.skills.sources.addPath.prompt=Kilo becerilerini içeren bir klasör seçin.
|
||||||
|
settings.agentBehavior.skills.sources.addUrl=URL ekle
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.title=Beceri Kaynağı URL'si Ekle
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.prompt=Bir beceri kaynağı URL'si girin.
|
||||||
|
settings.agentBehavior.skills.sources.editPath.title=Beceri Yolunu Düzenle
|
||||||
|
settings.agentBehavior.skills.sources.editUrl.title=Beceri URL'sini Düzenle
|
||||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||||
revert.banner.count.one={0} mesaj geri alındı
|
revert.banner.count.one={0} mesaj geri alındı
|
||||||
@@ -376,3 +403,26 @@ revert.banner.redo=Yinele
|
|||||||
revert.banner.redo.all=Tümünü yinele
|
revert.banner.redo.all=Tümünü yinele
|
||||||
revert.banner.hint=Yeni bir mesaj gönderene kadar bu değişiklikleri yineleyebilirsiniz
|
revert.banner.hint=Yeni bir mesaj gönderene kadar bu değişiklikleri yineleyebilirsiniz
|
||||||
revert.message.rollback=Bu mesaja geri dön
|
revert.message.rollback=Bu mesaja geri dön
|
||||||
|
|
||||||
|
# Context settings
|
||||||
|
settings.context.displayName=Bağlam
|
||||||
|
settings.context.description=Bağlam davranışını yapılandırın.
|
||||||
|
settings.context.save.pending=Bağlam ayarları kaydediliyor...
|
||||||
|
settings.context.save.failed=Bağlam ayarları kaydedilemedi
|
||||||
|
settings.context.compaction.title=Sıkıştırma
|
||||||
|
settings.context.compaction.auto.title=Otomatik Sıkıştırma
|
||||||
|
settings.context.compaction.auto.description=Bağlam sınıra ulaşmadan önce otomatik olarak sıkıştır
|
||||||
|
settings.context.compaction.threshold.title=Otomatik sıkıştırma sınırı
|
||||||
|
settings.context.compaction.threshold.description=Bağlam model penceresinin bu yüzdesine ulaştığında sıkıştır. Yalnızca güvenlik tamponunu kullanmak için boş bırakın.
|
||||||
|
settings.context.compaction.threshold.placeholder=Varsayılan
|
||||||
|
settings.context.compaction.threshold.suffix=%
|
||||||
|
settings.context.compaction.threshold.invalid=0 ile 100 arasında bir sayı girin veya alanı boş bırakın.
|
||||||
|
settings.context.compaction.prune.title=Eski Çıktıları Temizle
|
||||||
|
settings.context.compaction.prune.description=Sıkıştırma sırasında eski araç çıktılarını kaldır
|
||||||
|
settings.context.watcher.title=Dosya İzleyici Yok Sayma Kalıpları
|
||||||
|
settings.context.watcher.description=İzleyicinin yok sayması gereken dosyalar için glob kalıpları
|
||||||
|
settings.context.watcher.add=Kalıp ekle
|
||||||
|
settings.context.watcher.remove=Seçili kalıpları kaldır
|
||||||
|
settings.context.watcher.empty=Yok sayma kalıbı yapılandırılmadı.
|
||||||
|
settings.context.watcher.input.title=Yok sayma kalıbı ekle
|
||||||
|
settings.context.watcher.input.prompt=Yok sayılacak bir glob kalıbı girin:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
common.delete=Видалити
|
common.delete=Видалити
|
||||||
|
common.open=Відкрити
|
||||||
|
common.save=Зберегти
|
||||||
session.action.cancel=Скасувати
|
session.action.cancel=Скасувати
|
||||||
session.connection.connecting=Завантаження…
|
session.connection.connecting=Завантаження…
|
||||||
session.connection.error.app=Помилка з'єднання
|
session.connection.error.app=Помилка з'єднання
|
||||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
|||||||
|
|
||||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||||
settings.agentBehavior.mcp.addHint=Щоб додати сервер MCP, попросіть агента зробити це.
|
settings.agentBehavior.mcp.addHint=Щоб додати сервер MCP, попросіть агента зробити це.
|
||||||
|
settings.agentBehavior.skills.displayName=Навички
|
||||||
|
settings.agentBehavior.skills.search=Фільтрувати навички
|
||||||
|
settings.agentBehavior.skills.empty=Навички не знайдено.
|
||||||
|
settings.agentBehavior.skills.content.empty=Немає доступного вмісту навички.
|
||||||
|
settings.agentBehavior.skills.load.timeout=Час завантаження навичок минув. Наявні навички збережено; видаліть повільні або недоступні URL-адреси й оновіть.
|
||||||
|
settings.agentBehavior.skills.reload.deferred=Джерело навичок збережено. Перезавантажте Core після завершення активних сеансів, щоб застосувати нові навички.
|
||||||
|
settings.agentBehavior.skills.reload.blocked=Налаштування навичок збережено, але є активні сеанси. Перезавантажте Core після завершення цих сеансів, щоб застосувати нові навички.
|
||||||
|
settings.agentBehavior.skills.saved.notification=Налаштування навичок збережено
|
||||||
|
settings.agentBehavior.skills.delete.title=Видалити навичку
|
||||||
|
settings.agentBehavior.skills.delete.message=Видалити навичку {0}? Це видалить файл навички, і дію не можна буде скасувати.
|
||||||
|
settings.agentBehavior.skills.delete.failed=Не вдалося видалити навичку.
|
||||||
|
settings.agentBehavior.skills.openInEditor=Відкрити в редакторі
|
||||||
|
settings.agentBehavior.skills.openInEditor.pending=Файл навички відкриється після закриття Налаштувань.
|
||||||
|
settings.agentBehavior.skills.openInEditor.failed=Не вдалося відкрити файл навички в редакторі.
|
||||||
|
settings.agentBehavior.skills.sources.empty=Джерела навичок не налаштовано.
|
||||||
|
settings.agentBehavior.skills.sources.title=Додаткові джерела навичок
|
||||||
|
settings.agentBehavior.skills.sources.add=Додати
|
||||||
|
settings.agentBehavior.skills.sources.addPath=Додати шлях
|
||||||
|
settings.agentBehavior.skills.sources.addPath.title=Додати шлях до навичок
|
||||||
|
settings.agentBehavior.skills.sources.addPath.prompt=Виберіть папку з навичками Kilo.
|
||||||
|
settings.agentBehavior.skills.sources.addUrl=Додати URL
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.title=Додати URL джерела навичок
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.prompt=Введіть URL джерела навичок.
|
||||||
|
settings.agentBehavior.skills.sources.editPath.title=Редагувати шлях до навичок
|
||||||
|
settings.agentBehavior.skills.sources.editUrl.title=Редагувати URL навичок
|
||||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||||
revert.banner.count.one=Відкочено повідомлень: {0}
|
revert.banner.count.one=Відкочено повідомлень: {0}
|
||||||
@@ -376,3 +403,26 @@ revert.banner.redo=Повторити
|
|||||||
revert.banner.redo.all=Повторити все
|
revert.banner.redo.all=Повторити все
|
||||||
revert.banner.hint=Ці зміни можна повторити, доки ви не надішлете нове повідомлення
|
revert.banner.hint=Ці зміни можна повторити, доки ви не надішлете нове повідомлення
|
||||||
revert.message.rollback=Відкотитися до цього повідомлення
|
revert.message.rollback=Відкотитися до цього повідомлення
|
||||||
|
|
||||||
|
# Context settings
|
||||||
|
settings.context.displayName=Контекст
|
||||||
|
settings.context.description=Налаштуйте поведінку контексту.
|
||||||
|
settings.context.save.pending=Збереження налаштувань контексту...
|
||||||
|
settings.context.save.failed=Не вдалося зберегти налаштування контексту
|
||||||
|
settings.context.compaction.title=Стискання
|
||||||
|
settings.context.compaction.auto.title=Автоматичне стиснення
|
||||||
|
settings.context.compaction.auto.description=Автоматично стискати контекст до досягнення ліміту
|
||||||
|
settings.context.compaction.threshold.title=Ліміт автоматичного стискання
|
||||||
|
settings.context.compaction.threshold.description=Стискати, коли контекст досягає цього відсотка вікна моделі. Залиште порожнім, щоб використовувати лише буфер безпеки.
|
||||||
|
settings.context.compaction.threshold.placeholder=За замовчуванням
|
||||||
|
settings.context.compaction.threshold.suffix=%
|
||||||
|
settings.context.compaction.threshold.invalid=Введіть число від 0 до 100 або залиште поле порожнім.
|
||||||
|
settings.context.compaction.prune.title=Очищати старі виводи
|
||||||
|
settings.context.compaction.prune.description=Видаляти старі виводи інструментів під час стиснення
|
||||||
|
settings.context.watcher.title=Шаблони ігнорування спостерігача файлів
|
||||||
|
settings.context.watcher.description=Glob-шаблони для файлів, які спостерігач має ігнорувати
|
||||||
|
settings.context.watcher.add=Додати шаблон
|
||||||
|
settings.context.watcher.remove=Видалити вибрані шаблони
|
||||||
|
settings.context.watcher.empty=Шаблони ігнорування не налаштовано.
|
||||||
|
settings.context.watcher.input.title=Додати шаблон ігнорування
|
||||||
|
settings.context.watcher.input.prompt=Введіть glob-шаблон для ігнорування:
|
||||||
|
|||||||
+50
@@ -1,4 +1,6 @@
|
|||||||
common.delete=删除
|
common.delete=删除
|
||||||
|
common.open=打开
|
||||||
|
common.save=保存
|
||||||
session.action.cancel=取消
|
session.action.cancel=取消
|
||||||
session.connection.connecting=加载中…
|
session.connection.connecting=加载中…
|
||||||
session.connection.error.app=连接失败
|
session.connection.error.app=连接失败
|
||||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
|||||||
|
|
||||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||||
settings.agentBehavior.mcp.addHint=要添加 MCP 服务器,请让代理为你添加。
|
settings.agentBehavior.mcp.addHint=要添加 MCP 服务器,请让代理为你添加。
|
||||||
|
settings.agentBehavior.skills.displayName=技能
|
||||||
|
settings.agentBehavior.skills.search=筛选技能
|
||||||
|
settings.agentBehavior.skills.empty=未找到技能。
|
||||||
|
settings.agentBehavior.skills.content.empty=没有可用的技能内容。
|
||||||
|
settings.agentBehavior.skills.load.timeout=技能加载超时。已保留现有技能;请移除缓慢或无法访问的 URL 后刷新。
|
||||||
|
settings.agentBehavior.skills.reload.deferred=技能源已保存。请在活动会话结束后重新加载 Core,以应用新技能。
|
||||||
|
settings.agentBehavior.skills.reload.blocked=技能设置已保存,但仍有活动会话。请在这些会话结束后重新加载 Core,以应用新技能。
|
||||||
|
settings.agentBehavior.skills.saved.notification=技能设置已保存
|
||||||
|
settings.agentBehavior.skills.delete.title=删除技能
|
||||||
|
settings.agentBehavior.skills.delete.message=要删除技能 {0} 吗?这会移除技能文件,且无法撤销。
|
||||||
|
settings.agentBehavior.skills.delete.failed=无法删除该技能。
|
||||||
|
settings.agentBehavior.skills.openInEditor=在编辑器中打开
|
||||||
|
settings.agentBehavior.skills.openInEditor.pending=关闭设置后将打开技能文件。
|
||||||
|
settings.agentBehavior.skills.openInEditor.failed=无法在编辑器中打开技能文件。
|
||||||
|
settings.agentBehavior.skills.sources.empty=未配置技能源。
|
||||||
|
settings.agentBehavior.skills.sources.title=其他技能源
|
||||||
|
settings.agentBehavior.skills.sources.add=添加
|
||||||
|
settings.agentBehavior.skills.sources.addPath=添加路径
|
||||||
|
settings.agentBehavior.skills.sources.addPath.title=添加技能路径
|
||||||
|
settings.agentBehavior.skills.sources.addPath.prompt=选择一个包含 Kilo 技能的文件夹。
|
||||||
|
settings.agentBehavior.skills.sources.addUrl=添加 URL
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.title=添加技能源 URL
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.prompt=输入技能源 URL。
|
||||||
|
settings.agentBehavior.skills.sources.editPath.title=编辑技能路径
|
||||||
|
settings.agentBehavior.skills.sources.editUrl.title=编辑技能 URL
|
||||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||||
revert.banner.count.one=已回滚 {0} 条消息
|
revert.banner.count.one=已回滚 {0} 条消息
|
||||||
@@ -376,3 +403,26 @@ revert.banner.redo=重做
|
|||||||
revert.banner.redo.all=全部重做
|
revert.banner.redo.all=全部重做
|
||||||
revert.banner.hint=在发送新消息之前,你可以重做这些更改
|
revert.banner.hint=在发送新消息之前,你可以重做这些更改
|
||||||
revert.message.rollback=回滚到此消息
|
revert.message.rollback=回滚到此消息
|
||||||
|
|
||||||
|
# Context settings
|
||||||
|
settings.context.displayName=上下文
|
||||||
|
settings.context.description=配置上下文行为。
|
||||||
|
settings.context.save.pending=正在保存上下文设置...
|
||||||
|
settings.context.save.failed=保存上下文设置失败
|
||||||
|
settings.context.compaction.title=压缩
|
||||||
|
settings.context.compaction.auto.title=自动压缩
|
||||||
|
settings.context.compaction.auto.description=在上下文达到限制前自动压缩
|
||||||
|
settings.context.compaction.threshold.title=自动压缩限制
|
||||||
|
settings.context.compaction.threshold.description=当上下文达到模型窗口的此百分比时进行压缩。留空则仅使用安全缓冲区。
|
||||||
|
settings.context.compaction.threshold.placeholder=默认
|
||||||
|
settings.context.compaction.threshold.suffix=%
|
||||||
|
settings.context.compaction.threshold.invalid=输入 0 到 100 之间的数字,或将字段留空。
|
||||||
|
settings.context.compaction.prune.title=修剪旧输出
|
||||||
|
settings.context.compaction.prune.description=压缩期间移除旧的工具输出
|
||||||
|
settings.context.watcher.title=文件监视器忽略模式
|
||||||
|
settings.context.watcher.description=监视器应忽略的文件的 glob 模式
|
||||||
|
settings.context.watcher.add=添加模式
|
||||||
|
settings.context.watcher.remove=移除所选模式
|
||||||
|
settings.context.watcher.empty=未配置忽略模式。
|
||||||
|
settings.context.watcher.input.title=添加忽略模式
|
||||||
|
settings.context.watcher.input.prompt=输入要忽略的 glob 模式:
|
||||||
|
|||||||
+50
@@ -1,4 +1,6 @@
|
|||||||
common.delete=刪除
|
common.delete=刪除
|
||||||
|
common.open=開啟
|
||||||
|
common.save=儲存
|
||||||
session.action.cancel=取消
|
session.action.cancel=取消
|
||||||
session.connection.connecting=載入中…
|
session.connection.connecting=載入中…
|
||||||
session.connection.error.app=連線失敗
|
session.connection.error.app=連線失敗
|
||||||
@@ -368,6 +370,31 @@ settings.agentBehavior.undo=Undo
|
|||||||
|
|
||||||
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
settings.agentBehavior.agents.create.failed=Could not create the agent.
|
||||||
settings.agentBehavior.mcp.addHint=若要新增 MCP 伺服器,請請代理為你新增。
|
settings.agentBehavior.mcp.addHint=若要新增 MCP 伺服器,請請代理為你新增。
|
||||||
|
settings.agentBehavior.skills.displayName=技能
|
||||||
|
settings.agentBehavior.skills.search=篩選技能
|
||||||
|
settings.agentBehavior.skills.empty=找不到技能。
|
||||||
|
settings.agentBehavior.skills.content.empty=沒有可用的技能內容。
|
||||||
|
settings.agentBehavior.skills.load.timeout=技能載入逾時。已保留現有技能;請移除緩慢或無法連線的 URL 後重新整理。
|
||||||
|
settings.agentBehavior.skills.reload.deferred=技能來源已儲存。請在作用中工作階段結束後重新載入 Core,以套用新技能。
|
||||||
|
settings.agentBehavior.skills.reload.blocked=技能設定已儲存,但仍有作用中工作階段。請在這些工作階段結束後重新載入 Core,以套用新技能。
|
||||||
|
settings.agentBehavior.skills.saved.notification=技能設定已儲存
|
||||||
|
settings.agentBehavior.skills.delete.title=刪除技能
|
||||||
|
settings.agentBehavior.skills.delete.message=要刪除技能 {0} 嗎?這會移除技能檔案,且無法復原。
|
||||||
|
settings.agentBehavior.skills.delete.failed=無法刪除該技能。
|
||||||
|
settings.agentBehavior.skills.openInEditor=在編輯器中開啟
|
||||||
|
settings.agentBehavior.skills.openInEditor.pending=關閉設定後將開啟技能檔案。
|
||||||
|
settings.agentBehavior.skills.openInEditor.failed=無法在編輯器中開啟技能檔案。
|
||||||
|
settings.agentBehavior.skills.sources.empty=未設定技能來源。
|
||||||
|
settings.agentBehavior.skills.sources.title=其他技能來源
|
||||||
|
settings.agentBehavior.skills.sources.add=新增
|
||||||
|
settings.agentBehavior.skills.sources.addPath=新增路徑
|
||||||
|
settings.agentBehavior.skills.sources.addPath.title=新增技能路徑
|
||||||
|
settings.agentBehavior.skills.sources.addPath.prompt=選擇包含 Kilo 技能的資料夾。
|
||||||
|
settings.agentBehavior.skills.sources.addUrl=新增 URL
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.title=新增技能來源 URL
|
||||||
|
settings.agentBehavior.skills.sources.addUrl.prompt=輸入技能來源 URL。
|
||||||
|
settings.agentBehavior.skills.sources.editPath.title=編輯技能路徑
|
||||||
|
settings.agentBehavior.skills.sources.editUrl.title=編輯技能 URL
|
||||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||||
revert.banner.count.one=已回復 {0} 則訊息
|
revert.banner.count.one=已回復 {0} 則訊息
|
||||||
@@ -376,3 +403,26 @@ revert.banner.redo=重做
|
|||||||
revert.banner.redo.all=全部重做
|
revert.banner.redo.all=全部重做
|
||||||
revert.banner.hint=在傳送新訊息之前,你可以重做這些變更
|
revert.banner.hint=在傳送新訊息之前,你可以重做這些變更
|
||||||
revert.message.rollback=回復到此訊息
|
revert.message.rollback=回復到此訊息
|
||||||
|
|
||||||
|
# Context settings
|
||||||
|
settings.context.displayName=上下文
|
||||||
|
settings.context.description=設定上下文行為。
|
||||||
|
settings.context.save.pending=正在儲存上下文設定...
|
||||||
|
settings.context.save.failed=無法儲存上下文設定
|
||||||
|
settings.context.compaction.title=壓縮
|
||||||
|
settings.context.compaction.auto.title=自動壓縮
|
||||||
|
settings.context.compaction.auto.description=在上下文達到限制前自動壓縮
|
||||||
|
settings.context.compaction.threshold.title=自動壓縮限制
|
||||||
|
settings.context.compaction.threshold.description=當上下文達到模型視窗的此百分比時進行壓縮。留空則僅使用安全緩衝區。
|
||||||
|
settings.context.compaction.threshold.placeholder=預設
|
||||||
|
settings.context.compaction.threshold.suffix=%
|
||||||
|
settings.context.compaction.threshold.invalid=輸入 0 到 100 之間的數字,或將欄位留空。
|
||||||
|
settings.context.compaction.prune.title=修剪舊輸出
|
||||||
|
settings.context.compaction.prune.description=壓縮期間移除舊的工具輸出
|
||||||
|
settings.context.watcher.title=檔案監視器忽略模式
|
||||||
|
settings.context.watcher.description=監視器應忽略的檔案的 glob 模式
|
||||||
|
settings.context.watcher.add=新增模式
|
||||||
|
settings.context.watcher.remove=移除所選模式
|
||||||
|
settings.context.watcher.empty=尚未設定忽略模式。
|
||||||
|
settings.context.watcher.input.title=新增忽略模式
|
||||||
|
settings.context.watcher.input.prompt=輸入要忽略的 glob 模式:
|
||||||
|
|||||||
+31
@@ -3,6 +3,7 @@ package ai.kilocode.client.app
|
|||||||
import ai.kilocode.client.testing.FakeAgentBehaviorRpcApi
|
import ai.kilocode.client.testing.FakeAgentBehaviorRpcApi
|
||||||
import ai.kilocode.rpc.dto.AgentCreateDto
|
import ai.kilocode.rpc.dto.AgentCreateDto
|
||||||
import ai.kilocode.rpc.dto.McpStatusDto
|
import ai.kilocode.rpc.dto.McpStatusDto
|
||||||
|
import ai.kilocode.rpc.dto.SkillDto
|
||||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -10,6 +11,7 @@ import kotlinx.coroutines.SupervisorJob
|
|||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
class KiloAgentBehaviorServiceTest : BasePlatformTestCase() {
|
class KiloAgentBehaviorServiceTest : BasePlatformTestCase() {
|
||||||
private lateinit var scope: CoroutineScope
|
private lateinit var scope: CoroutineScope
|
||||||
@@ -67,6 +69,35 @@ class KiloAgentBehaviorServiceTest : BasePlatformTestCase() {
|
|||||||
assertTrue(rpc.removals.isEmpty())
|
assertTrue(rpc.removals.isEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun `test loadSkills propagates rpc failure`() = runBlocking {
|
||||||
|
rpc.skillsError = RuntimeException("boom")
|
||||||
|
|
||||||
|
assertFailsWith<RuntimeException> {
|
||||||
|
withContext(Dispatchers.Default) { service.loadSkills("/test") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test refreshSkills returns previous rows on rpc failure`() = runBlocking {
|
||||||
|
val fallback = listOf(SkillDto("plan", location = "/test/SKILL.md"))
|
||||||
|
rpc.skillsError = RuntimeException("boom")
|
||||||
|
|
||||||
|
val items = withContext(Dispatchers.Default) { service.refreshSkills("/test", fallback) }
|
||||||
|
|
||||||
|
assertEquals(fallback, items)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test saveSkills forwards all edits`() = runBlocking {
|
||||||
|
rpc.skills = listOf(SkillDto("plan", location = "/test/plan/SKILL.md"))
|
||||||
|
|
||||||
|
val ok = withContext(Dispatchers.Default) {
|
||||||
|
service.saveSkills("/test", mapOf("/test/plan/SKILL.md" to "# Saved"))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(ok)
|
||||||
|
assertEquals(listOf(Triple("/test", "/test/plan/SKILL.md", "# Saved")), rpc.skillSaves)
|
||||||
|
assertEquals("# Saved", rpc.skills.single().content)
|
||||||
|
}
|
||||||
|
|
||||||
fun `test mcpStatus forwards directory`() = runBlocking {
|
fun `test mcpStatus forwards directory`() = runBlocking {
|
||||||
rpc.mcps = listOf(McpStatusDto("filesystem", "connected"))
|
rpc.mcps = listOf(McpStatusDto("filesystem", "connected"))
|
||||||
|
|
||||||
|
|||||||
+8
@@ -97,4 +97,12 @@ class KiloWorkspaceServiceTest : BasePlatformTestCase() {
|
|||||||
assertEquals(err.message, seen?.message)
|
assertEquals(err.message, seen?.message)
|
||||||
assertEquals(listOf("dep"), rpc.searchQueries)
|
assertEquals(listOf("dep"), rpc.searchQueries)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun `test searchFiles sends query to RPC`() = runBlocking {
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
service.searchFiles("/test", "src")
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(listOf("src"), rpc.searchQueries)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-26
@@ -44,7 +44,6 @@ import com.intellij.openapi.actionSystem.UiDataProvider
|
|||||||
import com.intellij.openapi.actionSystem.ex.ActionUtil
|
import com.intellij.openapi.actionSystem.ex.ActionUtil
|
||||||
import com.intellij.openapi.application.ApplicationManager
|
import com.intellij.openapi.application.ApplicationManager
|
||||||
import com.intellij.openapi.command.WriteCommandAction
|
import com.intellij.openapi.command.WriteCommandAction
|
||||||
import com.intellij.openapi.command.undo.UndoManager
|
|
||||||
import com.intellij.openapi.editor.DefaultLanguageHighlighterColors
|
import com.intellij.openapi.editor.DefaultLanguageHighlighterColors
|
||||||
import com.intellij.openapi.editor.Editor
|
import com.intellij.openapi.editor.Editor
|
||||||
import com.intellij.openapi.editor.EditorFactory
|
import com.intellij.openapi.editor.EditorFactory
|
||||||
@@ -56,7 +55,6 @@ import com.intellij.openapi.editor.colors.EditorColorsManager
|
|||||||
import com.intellij.openapi.editor.colors.EditorColorsScheme
|
import com.intellij.openapi.editor.colors.EditorColorsScheme
|
||||||
import com.intellij.openapi.editor.ex.EditorEx
|
import com.intellij.openapi.editor.ex.EditorEx
|
||||||
import com.intellij.openapi.editor.markup.TextAttributes
|
import com.intellij.openapi.editor.markup.TextAttributes
|
||||||
import com.intellij.openapi.fileEditor.TextEditor
|
|
||||||
import com.intellij.openapi.ide.CopyPasteManager
|
import com.intellij.openapi.ide.CopyPasteManager
|
||||||
import com.intellij.openapi.fileTypes.PlainTextFileType
|
import com.intellij.openapi.fileTypes.PlainTextFileType
|
||||||
import com.intellij.openapi.fileTypes.PlainTextLanguage
|
import com.intellij.openapi.fileTypes.PlainTextLanguage
|
||||||
@@ -455,28 +453,20 @@ class PromptPanelTest : BasePlatformTestCase() {
|
|||||||
assertTrue(spans.contains("@unknown" to CodeInsightColors.WRONG_REFERENCES_ATTRIBUTES))
|
assertTrue(spans.contains("@unknown" to CodeInsightColors.WRONG_REFERENCES_ATTRIBUTES))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun `test prompt editor exposes file editor for undo redo`() {
|
fun `test prompt editor does not expose file editor for platform undo`() {
|
||||||
val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> }, completion = completion())
|
val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> }, completion = completion())
|
||||||
val field = panel.defaultFocusedComponent as EditorTextField
|
val field = panel.defaultFocusedComponent as EditorTextField
|
||||||
|
|
||||||
realize(panel, 260, 400)
|
realize(panel, 260, 400)
|
||||||
val editor = field.getEditor(false)!!
|
val editor = field.getEditor(false)!!
|
||||||
WriteCommandAction.runWriteCommandAction(project) {
|
|
||||||
editor.document.insertString(0, "hello")
|
|
||||||
}
|
|
||||||
val sink = TestSink()
|
val sink = TestSink()
|
||||||
(field as UiDataProvider).uiDataSnapshot(sink)
|
(field as UiDataProvider).uiDataSnapshot(sink)
|
||||||
val file = sink.file as? TextEditor ?: error("missing file editor")
|
|
||||||
|
|
||||||
assertNotNull(file)
|
assertNull(sink.file)
|
||||||
assertSame(editor.document, file.editor.document)
|
assertSame(true, editor.getUserData(EditorTextField.SUPPLEMENTARY_KEY))
|
||||||
UndoManager.getInstance(project).undo(file)
|
|
||||||
assertEquals("", editor.document.text)
|
|
||||||
UndoManager.getInstance(project).redo(file)
|
|
||||||
assertEquals("hello", editor.document.text)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun `test prompt editor platform undo redo actions target prompt editor`() {
|
fun `test prompt editor platform undo redo actions do not throw`() {
|
||||||
val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> }, completion = completion())
|
val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> }, completion = completion())
|
||||||
val field = panel.defaultFocusedComponent as EditorTextField
|
val field = panel.defaultFocusedComponent as EditorTextField
|
||||||
|
|
||||||
@@ -488,13 +478,10 @@ class PromptPanelTest : BasePlatformTestCase() {
|
|||||||
assertSame(true, editor.contentComponent.getClientProperty(UndoRedoAction.IGNORE_SWING_UNDO_MANAGER))
|
assertSame(true, editor.contentComponent.getClientProperty(UndoRedoAction.IGNORE_SWING_UNDO_MANAGER))
|
||||||
val sink = TestSink()
|
val sink = TestSink()
|
||||||
(field as UiDataProvider).uiDataSnapshot(sink)
|
(field as UiDataProvider).uiDataSnapshot(sink)
|
||||||
val file = sink.file as? TextEditor ?: error("missing file editor")
|
assertNull(sink.file)
|
||||||
assertSame(editor.document, file.editor.document)
|
|
||||||
assertTrue("prompt file editor should have undo", UndoManager.getInstance(project).isUndoAvailable(file))
|
|
||||||
|
|
||||||
invokeAction(IdeActions.ACTION_UNDO, editor.contentComponent, file)
|
updatePlatformAction(IdeActions.ACTION_UNDO, editor)
|
||||||
assertEquals("", editor.document.text)
|
updatePlatformAction(IdeActions.ACTION_REDO, editor)
|
||||||
invokeAction(IdeActions.ACTION_REDO, editor.contentComponent, file)
|
|
||||||
assertEquals("hello", editor.document.text)
|
assertEquals("hello", editor.document.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1364,21 +1351,18 @@ class PromptPanelTest : BasePlatformTestCase() {
|
|||||||
UIUtil.dispatchAllInvocationEvents()
|
UIUtil.dispatchAllInvocationEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun invokeAction(id: String, component: java.awt.Component, file: TextEditor) {
|
private fun updatePlatformAction(id: String, editor: Editor) {
|
||||||
val action = ActionManager.getInstance().getAction(id) ?: error("missing action $id")
|
val action = ActionManager.getInstance().getAction(id) ?: error("missing action $id")
|
||||||
val ctx = DataContext { data ->
|
val ctx = DataContext { data ->
|
||||||
when (data) {
|
when (data) {
|
||||||
CommonDataKeys.PROJECT.name -> project
|
CommonDataKeys.PROJECT.name -> project
|
||||||
PlatformCoreDataKeys.CONTEXT_COMPONENT.name -> component
|
CommonDataKeys.EDITOR.name -> editor
|
||||||
PlatformCoreDataKeys.FILE_EDITOR.name -> file
|
PlatformCoreDataKeys.CONTEXT_COMPONENT.name -> editor.contentComponent
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val event = AnActionEvent.createEvent(action, ctx, null, ActionPlaces.UNKNOWN, ActionUiKind.NONE, null)
|
val event = AnActionEvent.createEvent(action, ctx, null, ActionPlaces.UNKNOWN, ActionUiKind.NONE, null)
|
||||||
ActionUtil.updateAction(action, event)
|
ActionUtil.updateAction(action, event)
|
||||||
assertTrue("action $id should be enabled", event.presentation.isEnabled)
|
|
||||||
ActionUtil.performAction(action, event)
|
|
||||||
UIUtil.dispatchAllInvocationEvents()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun waitForLookupItems(editor: Editor): List<String> {
|
private fun waitForLookupItems(editor: Editor): List<String> {
|
||||||
|
|||||||
+15
-1
@@ -1,6 +1,7 @@
|
|||||||
package ai.kilocode.client.settings
|
package ai.kilocode.client.settings
|
||||||
|
|
||||||
import ai.kilocode.client.settings.profile.UserProfileConfigurable
|
import ai.kilocode.client.settings.profile.UserProfileConfigurable
|
||||||
|
import ai.kilocode.client.settings.context.ContextConfigurable
|
||||||
import ai.kilocode.client.settings.models.ModelsConfigurable
|
import ai.kilocode.client.settings.models.ModelsConfigurable
|
||||||
import ai.kilocode.client.settings.agents.AgentBehaviorConfigurable
|
import ai.kilocode.client.settings.agents.AgentBehaviorConfigurable
|
||||||
import ai.kilocode.client.settings.providers.ProvidersConfigurable
|
import ai.kilocode.client.settings.providers.ProvidersConfigurable
|
||||||
@@ -29,6 +30,10 @@ class KiloSettingsConfigurableTest : BasePlatformTestCase() {
|
|||||||
assertEquals("ai.kilocode.jetbrains.settings.models", ModelsConfigurable.ID)
|
assertEquals("ai.kilocode.jetbrains.settings.models", ModelsConfigurable.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun `test child context id matches xml registration`() {
|
||||||
|
assertEquals("ai.kilocode.jetbrains.settings.context", ContextConfigurable.ID)
|
||||||
|
}
|
||||||
|
|
||||||
fun `test child provider and behavior ids match xml registration`() {
|
fun `test child provider and behavior ids match xml registration`() {
|
||||||
assertEquals("ai.kilocode.jetbrains.settings.providers", ProvidersConfigurable.ID)
|
assertEquals("ai.kilocode.jetbrains.settings.providers", ProvidersConfigurable.ID)
|
||||||
assertEquals("ai.kilocode.jetbrains.settings.agentBehavior", AgentBehaviorConfigurable.ID)
|
assertEquals("ai.kilocode.jetbrains.settings.agentBehavior", AgentBehaviorConfigurable.ID)
|
||||||
@@ -79,12 +84,21 @@ class KiloSettingsConfigurableTest : BasePlatformTestCase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun `test createComponent contains Context link`() {
|
||||||
|
val cfg = KiloSettingsConfigurable()
|
||||||
|
edt {
|
||||||
|
val panel = cfg.createComponent()
|
||||||
|
val links = links(panel as Container)
|
||||||
|
assertTrue("expected a link labeled 'Context'", links.any { it.text == "Context" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun `test createComponent contains settings links in order`() {
|
fun `test createComponent contains settings links in order`() {
|
||||||
val cfg = KiloSettingsConfigurable()
|
val cfg = KiloSettingsConfigurable()
|
||||||
edt {
|
edt {
|
||||||
val panel = cfg.createComponent()
|
val panel = cfg.createComponent()
|
||||||
val labels = links(panel as Container).map { it.text }
|
val labels = links(panel as Container).map { it.text }
|
||||||
assertEquals(listOf("User Profile", "Models", "Providers", "Agent Behavior"), labels)
|
assertEquals(listOf("User Profile", "Models", "Providers", "Agent Behavior", "Context"), labels)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -18,6 +18,7 @@ class AgentBehaviorConfigurableTest : BasePlatformTestCase() {
|
|||||||
fun `test child ids match xml registration`() {
|
fun `test child ids match xml registration`() {
|
||||||
assertEquals("ai.kilocode.jetbrains.settings.agentBehavior.agents", AgentsConfigurable.ID)
|
assertEquals("ai.kilocode.jetbrains.settings.agentBehavior.agents", AgentsConfigurable.ID)
|
||||||
assertEquals("ai.kilocode.jetbrains.settings.agentBehavior.mcp", McpConfigurable.ID)
|
assertEquals("ai.kilocode.jetbrains.settings.agentBehavior.mcp", McpConfigurable.ID)
|
||||||
|
assertEquals("ai.kilocode.jetbrains.settings.agentBehavior.skills", SkillsConfigurable.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun `test createComponent contains child links in order`() {
|
fun `test createComponent contains child links in order`() {
|
||||||
@@ -26,7 +27,7 @@ class AgentBehaviorConfigurableTest : BasePlatformTestCase() {
|
|||||||
edt {
|
edt {
|
||||||
val panel = cfg.createComponent()
|
val panel = cfg.createComponent()
|
||||||
val labels = links(panel as Container).map { it.text }
|
val labels = links(panel as Container).map { it.text }
|
||||||
assertEquals(listOf("Agents", "MCP Servers"), labels)
|
assertEquals(listOf("Agents", "MCP Servers", "Skills"), labels)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+615
@@ -0,0 +1,615 @@
|
|||||||
|
package ai.kilocode.client.settings.agents
|
||||||
|
|
||||||
|
import ai.kilocode.client.app.KiloAgentBehaviorService
|
||||||
|
import ai.kilocode.client.app.KiloAppService
|
||||||
|
import ai.kilocode.client.app.KiloWorkspaceService
|
||||||
|
import ai.kilocode.client.settings.base.SettingsListItem
|
||||||
|
import ai.kilocode.client.settings.base.settingsListCellBounds
|
||||||
|
import ai.kilocode.client.testing.FakeAgentBehaviorRpcApi
|
||||||
|
import ai.kilocode.client.testing.FakeAppRpcApi
|
||||||
|
import ai.kilocode.client.testing.FakeWorkspaceRpcApi
|
||||||
|
import ai.kilocode.client.testing.fire
|
||||||
|
import ai.kilocode.rpc.dto.ConfigDto
|
||||||
|
import ai.kilocode.rpc.dto.KiloAppStateDto
|
||||||
|
import ai.kilocode.rpc.dto.KiloAppStatusDto
|
||||||
|
import ai.kilocode.rpc.dto.SkillDto
|
||||||
|
import ai.kilocode.rpc.dto.SkillsConfigDto
|
||||||
|
import com.intellij.openapi.application.ApplicationManager
|
||||||
|
import com.intellij.openapi.fileTypes.FileTypeManager
|
||||||
|
import com.intellij.openapi.fileTypes.PlainTextFileType
|
||||||
|
import com.intellij.openapi.fileTypes.UnknownFileType
|
||||||
|
import com.intellij.openapi.ui.DialogWrapper
|
||||||
|
import com.intellij.openapi.ui.Messages
|
||||||
|
import com.intellij.openapi.ui.TestDialog
|
||||||
|
import com.intellij.openapi.ui.TestDialogManager
|
||||||
|
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||||
|
import com.intellij.testFramework.replaceService
|
||||||
|
import com.intellij.ui.TitledSeparator
|
||||||
|
import com.intellij.ui.SimpleColoredComponent
|
||||||
|
import com.intellij.ui.components.JBLabel
|
||||||
|
import com.intellij.ui.components.JBList
|
||||||
|
import com.intellij.ui.components.JBScrollPane
|
||||||
|
import com.intellij.util.ui.UIUtil
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Container
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.Point
|
||||||
|
import java.awt.event.InputEvent
|
||||||
|
import java.awt.event.MouseEvent
|
||||||
|
import javax.swing.JComponent
|
||||||
|
import javax.swing.ScrollPaneConstants
|
||||||
|
import javax.swing.Scrollable
|
||||||
|
import javax.swing.JTextField
|
||||||
|
|
||||||
|
class SkillsSettingsUiTest : BasePlatformTestCase() {
|
||||||
|
private var scope: CoroutineScope? = null
|
||||||
|
private var ui: SkillsSettingsUi? = null
|
||||||
|
private lateinit var app: KiloAppService
|
||||||
|
private lateinit var appRpc: FakeAppRpcApi
|
||||||
|
private lateinit var agentRpc: FakeAgentBehaviorRpcApi
|
||||||
|
private lateinit var workspaceRpc: FakeWorkspaceRpcApi
|
||||||
|
private var shown = 0
|
||||||
|
|
||||||
|
override fun tearDown() {
|
||||||
|
try {
|
||||||
|
TestDialogManager.setTestDialog(TestDialog.DEFAULT)
|
||||||
|
ui?.let { panel -> edt { panel.dispose(); true } }
|
||||||
|
ui = null
|
||||||
|
scope?.cancel()
|
||||||
|
scope = null
|
||||||
|
} finally {
|
||||||
|
super.tearDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test loads skills with location note and builtins have no actions`() {
|
||||||
|
val panel = panel()
|
||||||
|
|
||||||
|
flushUntil { rows(panel).size == 3 }
|
||||||
|
|
||||||
|
edt {
|
||||||
|
val rows = rows(panel)
|
||||||
|
val custom = rows.single { it.key == CUSTOM }
|
||||||
|
assertEquals("plan", custom.title)
|
||||||
|
assertEquals(CUSTOM, custom.note)
|
||||||
|
assertEquals("Plan work", custom.description)
|
||||||
|
assertEquals("edit", custom.doubleClick)
|
||||||
|
assertEquals(listOf("open", "edit", "delete"), custom.cells.map { it.id })
|
||||||
|
assertTrue(custom.cells.single { it.id == "open" }.primary)
|
||||||
|
assertFalse(custom.cells.single { it.id == "edit" }.primary)
|
||||||
|
assertEquals("Edit", custom.cells.single { it.id == "edit" }.label)
|
||||||
|
assertTrue(custom.cells.single { it.id == "delete" }.iconOnly)
|
||||||
|
val builtin = rows.single { it.key == "builtin" }
|
||||||
|
assertEquals("thinking", builtin.title)
|
||||||
|
assertNull(builtin.note)
|
||||||
|
assertEquals("edit", builtin.doubleClick)
|
||||||
|
assertEquals(listOf("built-in"), builtin.badges.map { it.text })
|
||||||
|
assertEquals(listOf("edit"), builtin.cells.map { it.id })
|
||||||
|
assertEquals("Open", builtin.cells.single().label)
|
||||||
|
val remote = rows.single { it.key == REMOTE }
|
||||||
|
assertEquals(listOf("edit"), remote.cells.map { it.id })
|
||||||
|
assertEquals("Open", remote.cells.single().label)
|
||||||
|
assertEquals(listOf(DIR), agentRpc.skillCalls)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test skills list is vertically scrolled without horizontal scrollbar`() {
|
||||||
|
val panel = panel()
|
||||||
|
flushUntil { rows(panel).size == 3 }
|
||||||
|
|
||||||
|
edt {
|
||||||
|
val pane = scrollFor(panel, skillsList(panel))
|
||||||
|
val view = pane.viewport.view
|
||||||
|
val layout = panel.content.layout as BorderLayout
|
||||||
|
|
||||||
|
assertEquals(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER, pane.horizontalScrollBarPolicy)
|
||||||
|
assertTrue((view as Scrollable).getScrollableTracksViewportWidth())
|
||||||
|
assertFalse(view.getScrollableTracksViewportHeight())
|
||||||
|
assertSame(pane, layout.getLayoutComponent(BorderLayout.CENTER))
|
||||||
|
assertSame(panel.sources, layout.getLayoutComponent(BorderLayout.SOUTH))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test sources section has additional sources title`() {
|
||||||
|
val panel = panel()
|
||||||
|
flushUntil { sourceRows(panel).size == 2 }
|
||||||
|
|
||||||
|
assertTrue(edt {
|
||||||
|
components(panel).filterIsInstance<TitledSeparator>().any { it.text == "Additional Skill Sources" }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test skills list does not show description tooltips`() {
|
||||||
|
val panel = panel()
|
||||||
|
flushUntil { rows(panel).size == 3 }
|
||||||
|
|
||||||
|
edt {
|
||||||
|
val list = skillsList(panel)
|
||||||
|
list.size = Dimension(520, 320)
|
||||||
|
list.doLayout()
|
||||||
|
val bounds = list.getCellBounds(0, 0)
|
||||||
|
|
||||||
|
assertNull(list.getToolTipText(mouse(list, MouseEvent.MOUSE_MOVED, Point(bounds.x + 8, bounds.y + 8))))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test renderer puts location on first line and description on preview line`() {
|
||||||
|
val panel = panel()
|
||||||
|
flushUntil { rows(panel).size == 3 }
|
||||||
|
|
||||||
|
edt {
|
||||||
|
val list = skillsList(panel)
|
||||||
|
val row = rows(panel).single { it.key == CUSTOM }
|
||||||
|
val idx = rows(panel).indexOf(row)
|
||||||
|
val comp = list.cellRenderer.getListCellRendererComponent(list, row, idx, true, true)
|
||||||
|
comp.setSize(520, list.fixedCellHeight)
|
||||||
|
layout(comp)
|
||||||
|
val title = components(comp).filterIsInstance<SimpleColoredComponent>().single()
|
||||||
|
val labels = components(comp).filterIsInstance<JBLabel>().filter { it.isVisible }.map { it.text }
|
||||||
|
|
||||||
|
assertEquals("plan $CUSTOM", title.toString())
|
||||||
|
assertTrue(labels.contains("Plan work"))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test double click stages skill content until apply`() {
|
||||||
|
val panel = panel(edit = { _, _ -> FakeSkillDialog("# Saved") })
|
||||||
|
flushUntil { rows(panel).size == 3 }
|
||||||
|
|
||||||
|
doubleClick(skillsList(panel), panel, CUSTOM)
|
||||||
|
|
||||||
|
assertTrue(edt { panel.modified() })
|
||||||
|
assertTrue(agentRpc.skillSaves.isEmpty())
|
||||||
|
edt { panel.applyDraft(); true }
|
||||||
|
flushUntil { agentRpc.skillSaves.size == 1 }
|
||||||
|
assertEquals(Triple(DIR, CUSTOM, "# Saved"), agentRpc.skillSaves.single())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test edited skill row keeps normal actions`() {
|
||||||
|
val panel = panel(edit = { _, _ -> FakeSkillDialog("# Draft") })
|
||||||
|
flushUntil { rows(panel).size == 3 }
|
||||||
|
|
||||||
|
doubleClick(skillsList(panel), panel, CUSTOM)
|
||||||
|
|
||||||
|
assertEquals(listOf("open", "edit", "delete"), edt { rows(panel).single { it.key == CUSTOM }.cells.map { it.id } })
|
||||||
|
assertTrue(edt { panel.modified() })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test reopening staged skill edit shows draft content before apply`() {
|
||||||
|
val seen = mutableListOf<String?>()
|
||||||
|
val panel = panel(edit = { skill, _ ->
|
||||||
|
seen += skill.content
|
||||||
|
FakeSkillDialog(if (seen.size == 1) "# Draft" else "# Draft 2")
|
||||||
|
})
|
||||||
|
flushUntil { rows(panel).size == 3 }
|
||||||
|
|
||||||
|
doubleClick(skillsList(panel), panel, CUSTOM)
|
||||||
|
doubleClick(skillsList(panel), panel, CUSTOM)
|
||||||
|
|
||||||
|
assertEquals(listOf("# Plan\nUse steps", "# Draft"), seen)
|
||||||
|
assertTrue(agentRpc.skillSaves.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test open in editor action opens skill file`() {
|
||||||
|
val panel = panel()
|
||||||
|
flushUntil { rows(panel).size == 3 }
|
||||||
|
|
||||||
|
click(skillsList(panel), panel, CUSTOM, "open")
|
||||||
|
|
||||||
|
assertEquals("The skill file will open after you close Settings.", edt { progressText(panel) })
|
||||||
|
flushUntil { workspaceRpc.openedFiles.size == 1 }
|
||||||
|
assertEquals(FakeWorkspaceRpcApi.Opened(CUSTOM, null, null), workspaceRpc.openedFiles.single())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test read only skills open without staging edits or editor file open`() {
|
||||||
|
shown = 0
|
||||||
|
val panel = panel(edit = { _, savable ->
|
||||||
|
assertFalse(savable)
|
||||||
|
FakeSkillDialog("# Ignored") { shown += 1 }
|
||||||
|
})
|
||||||
|
flushUntil { rows(panel).size == 3 }
|
||||||
|
|
||||||
|
click(skillsList(panel), panel, REMOTE, "edit")
|
||||||
|
|
||||||
|
assertEquals(1, shown)
|
||||||
|
assertFalse(edt { panel.modified() })
|
||||||
|
assertTrue(agentRpc.skillSaves.isEmpty())
|
||||||
|
assertTrue(workspaceRpc.openedFiles.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test skill edit dialog shows content with fallback`() {
|
||||||
|
edt {
|
||||||
|
val content = SkillEditDialog(SkillDto("plan", "desc", CUSTOM, "# Plan\nUse steps"), true)
|
||||||
|
val fallback = SkillEditDialog(SkillDto("plan", "desc", CUSTOM), true)
|
||||||
|
val readonly = SkillEditDialog(SkillDto("kilo-config", "desc", "builtin", "<h1>Kilo Config</h1>"), false)
|
||||||
|
try {
|
||||||
|
assertEquals("# Plan\nUse steps", content.content())
|
||||||
|
assertEquals("desc", fallback.content())
|
||||||
|
assertEquals("<h1>Kilo Config</h1>", readonly.content())
|
||||||
|
assertEquals("OK", content.okText())
|
||||||
|
} finally {
|
||||||
|
content.close(DialogWrapper.CANCEL_EXIT_CODE)
|
||||||
|
fallback.close(DialogWrapper.CANCEL_EXIT_CODE)
|
||||||
|
readonly.close(DialogWrapper.CANCEL_EXIT_CODE)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test skill editor file type follows content syntax before location`() {
|
||||||
|
assertEquals(
|
||||||
|
FileTypeManager.getInstance().getFileTypeByFileName("index.html"),
|
||||||
|
skillFileType("builtin", "<h1>Kilo CLI Configuration Reference</h1><p>All config lives in <code>kilo.json</code>.</p>"),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
skillFileType("SKILL.md"),
|
||||||
|
skillFileType("builtin", "# Kilo CLI Configuration Reference\n\nAll config lives in `kilo.json`."),
|
||||||
|
)
|
||||||
|
assertEquals(PlainTextFileType.INSTANCE, skillFileType("builtin", "Plain fallback text"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun `test delete action stages skill removal until apply`() {
|
||||||
|
val panel = panel()
|
||||||
|
flushUntil { rows(panel).size == 3 }
|
||||||
|
TestDialogManager.setTestDialog(TestDialog.YES)
|
||||||
|
|
||||||
|
click(skillsList(panel), panel, CUSTOM, "delete")
|
||||||
|
|
||||||
|
assertTrue(edt { rows(panel).none { it.key == CUSTOM } })
|
||||||
|
assertTrue(agentRpc.skillRemovals.isEmpty())
|
||||||
|
edt { panel.applyDraft(); true }
|
||||||
|
flushUntil { agentRpc.skillRemovals.size == 1 }
|
||||||
|
assertEquals(listOf(DIR to CUSTOM), agentRpc.skillRemovals)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test delete action requires confirmation`() {
|
||||||
|
val panel = panel()
|
||||||
|
flushUntil { rows(panel).size == 3 }
|
||||||
|
TestDialogManager.setTestDialog { Messages.NO }
|
||||||
|
|
||||||
|
click(skillsList(panel), panel, CUSTOM, "delete")
|
||||||
|
|
||||||
|
edt { UIUtil.dispatchAllInvocationEvents(); true }
|
||||||
|
assertTrue(agentRpc.skillRemovals.isEmpty())
|
||||||
|
assertTrue(edt { rows(panel).any { it.key == CUSTOM } })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test add path and url write skills config patch on apply`() {
|
||||||
|
var path = "/extra/skills"
|
||||||
|
var url = "https://skills.test/index.json"
|
||||||
|
val panel = panel(choose = { path }, input = { _, _ -> url })
|
||||||
|
flushUntil { rows(panel).size == 3 }
|
||||||
|
|
||||||
|
edt { panel.sources.addPath(); true }
|
||||||
|
edt { panel.sources.addUrl(); true }
|
||||||
|
flushUntil { sourceRows(panel).any { it.key == "url:$url" } }
|
||||||
|
assertTrue(appRpc.configPatches.isEmpty())
|
||||||
|
|
||||||
|
edt { panel.applyDraft(); true }
|
||||||
|
flushUntil { appRpc.configPatches.size == 1 && !edt { panel.modified() } }
|
||||||
|
|
||||||
|
val paths = appRpc.configPatches.single().skills!!.paths
|
||||||
|
val urls = appRpc.configPatches.single().skills!!.urls
|
||||||
|
assertEquals(listOf("/global/skills", path), paths)
|
||||||
|
assertEquals(listOf("https://skills.test/base.json", url), urls)
|
||||||
|
assertEquals(
|
||||||
|
listOf("path:/global/skills", "path:$path", "url:https://skills.test/base.json", "url:$url"),
|
||||||
|
edt { sourceRows(panel).map { it.key } },
|
||||||
|
)
|
||||||
|
assertEquals(listOf(DIR), agentRpc.skillReloads)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test stale config update result keeps added skill sources visible`() {
|
||||||
|
val path = "/extra/skills"
|
||||||
|
val url = "https://skills.test/index.json"
|
||||||
|
val extra = "$path/extra/SKILL.md"
|
||||||
|
val panel = panel(choose = { path }, input = { _, _ -> url })
|
||||||
|
appRpc.configUpdateReturnStale = true
|
||||||
|
appRpc.afterConfig = { agentRpc.skills = agentRpc.skills + SkillDto("extra", "Extra skill", extra) }
|
||||||
|
flushUntil { rows(panel).size == 3 }
|
||||||
|
|
||||||
|
edt {
|
||||||
|
panel.sources.addPath()
|
||||||
|
panel.sources.addUrl()
|
||||||
|
panel.applyDraft()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
flushUntil { appRpc.configPatches.size == 1 && !edt { panel.modified() } }
|
||||||
|
assertTrue(edt { rows(panel).any { it.key == extra } })
|
||||||
|
assertEquals(
|
||||||
|
listOf("path:/global/skills", "path:$path", "url:https://skills.test/base.json", "url:$url"),
|
||||||
|
edt { sourceRows(panel).map { it.key } },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test blocked reload completes apply with warning`() {
|
||||||
|
val path = "/extra/skills"
|
||||||
|
val panel = panel(choose = { path })
|
||||||
|
agentRpc.reloadSkillResult = false
|
||||||
|
flushUntil { rows(panel).size == 3 }
|
||||||
|
|
||||||
|
edt {
|
||||||
|
panel.sources.addPath()
|
||||||
|
panel.applyDraft()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
flushUntil { appRpc.configPatches.size == 1 && !edt { panel.modified() } }
|
||||||
|
assertEquals(listOf(DIR), agentRpc.skillReloads)
|
||||||
|
assertEquals("Skills settings saved, but active sessions are present. Reload the core after those sessions finish to apply the new skills.", edt { progressText(panel) })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test post apply skills refresh failure keeps saved rows`() {
|
||||||
|
val panel = panel(edit = { _, _ -> FakeSkillDialog("# Saved") })
|
||||||
|
flushUntil { rows(panel).size == 3 }
|
||||||
|
|
||||||
|
doubleClick(skillsList(panel), panel, CUSTOM)
|
||||||
|
agentRpc.skillsError = RuntimeException("timeout")
|
||||||
|
edt { panel.applyDraft(); true }
|
||||||
|
|
||||||
|
flushUntil { agentRpc.skillSaves.size == 1 && !edt { panel.modified() } }
|
||||||
|
assertEquals(listOf(CUSTOM, "builtin", REMOTE), edt { rows(panel).map { it.key } })
|
||||||
|
assertEquals("# Saved", agentRpc.skills.single { it.location == CUSTOM }.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test source reset discards staged changes`() {
|
||||||
|
val path = "/extra/skills"
|
||||||
|
val panel = panel(choose = { path })
|
||||||
|
flushUntil { rows(panel).size == 3 }
|
||||||
|
|
||||||
|
edt { panel.sources.addPath(); true }
|
||||||
|
|
||||||
|
assertTrue(edt { sourceRows(panel).any { it.key == "path:$path" } })
|
||||||
|
assertTrue(edt { panel.modified() })
|
||||||
|
edt { panel.resetDraft(); true }
|
||||||
|
|
||||||
|
assertTrue(appRpc.configPatches.isEmpty())
|
||||||
|
assertEquals(listOf(CUSTOM, "builtin", REMOTE), edt { rows(panel).map { it.key } })
|
||||||
|
assertFalse(edt { sourceRows(panel).any { it.key == "path:$path" } })
|
||||||
|
assertTrue(agentRpc.skillReloads.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test delete source writes skills config patch`() {
|
||||||
|
val panel = panel()
|
||||||
|
flushUntil { rows(panel).size == 3 && sourceRows(panel).size == 2 }
|
||||||
|
|
||||||
|
edt {
|
||||||
|
sourceList(panel).selectedIndices = intArrayOf(0)
|
||||||
|
panel.sources.removeSelected()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(appRpc.configPatches.isEmpty())
|
||||||
|
edt { panel.applyDraft(); true }
|
||||||
|
flushUntil { appRpc.configPatches.size == 1 && !edt { panel.modified() } }
|
||||||
|
val patch = appRpc.configPatches.single().skills!!
|
||||||
|
assertEquals(emptyList<String>(), patch.paths)
|
||||||
|
assertEquals(listOf("https://skills.test/base.json"), patch.urls)
|
||||||
|
assertEquals(listOf("url:https://skills.test/base.json"), edt { sourceRows(panel).map { it.key } })
|
||||||
|
assertEquals(listOf(DIR), agentRpc.skillReloads)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test stale config update result keeps removed skill sources hidden`() {
|
||||||
|
val panel = panel()
|
||||||
|
appRpc.configUpdateReturnStale = true
|
||||||
|
appRpc.afterConfig = { agentRpc.skills = agentRpc.skills.filterNot { it.location == CUSTOM } }
|
||||||
|
flushUntil { rows(panel).size == 3 && sourceRows(panel).size == 2 }
|
||||||
|
|
||||||
|
edt {
|
||||||
|
sourceList(panel).selectedIndices = intArrayOf(0)
|
||||||
|
panel.sources.removeSelected()
|
||||||
|
panel.applyDraft()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
flushUntil { appRpc.configPatches.size == 1 && !edt { panel.modified() } }
|
||||||
|
assertEquals(listOf("builtin", REMOTE), edt { rows(panel).map { it.key } })
|
||||||
|
assertEquals(listOf("url:https://skills.test/base.json"), edt { sourceRows(panel).map { it.key } })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test search filters skills by name`() {
|
||||||
|
val panel = panel()
|
||||||
|
flushUntil { rows(panel).size == 3 }
|
||||||
|
|
||||||
|
edt {
|
||||||
|
components(panel).filterIsInstance<JTextField>().single().text = "think"
|
||||||
|
UIUtil.dispatchAllInvocationEvents()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
flushUntil { rows(panel).map { it.key } == listOf("builtin") }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test skills reload failure keeps existing rows`() {
|
||||||
|
val panel = panel()
|
||||||
|
flushUntil { rows(panel).size == 3 }
|
||||||
|
agentRpc.skillsError = RuntimeException("timeout")
|
||||||
|
|
||||||
|
edt { panel.reload(); true }
|
||||||
|
flushUntil { edt { skillsList(panel).isEnabled } }
|
||||||
|
|
||||||
|
assertEquals(listOf(CUSTOM, "builtin", REMOTE), edt { rows(panel).map { it.key } })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test skill editor file type follows location extension`() {
|
||||||
|
assertNotSame(UnknownFileType.INSTANCE, skillFileType("/tmp/skills/plan/SKILL.md"))
|
||||||
|
assertEquals(
|
||||||
|
FileTypeManager.getInstance().getFileTypeByFileName("index.html"),
|
||||||
|
skillFileType("/tmp/skills/index.html"),
|
||||||
|
)
|
||||||
|
assertEquals(PlainTextFileType.INSTANCE, skillFileType("/tmp/skills/index.unknown"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun `test skill path chooser accepts directories only`() {
|
||||||
|
val descriptor = skillPathDescriptor()
|
||||||
|
|
||||||
|
assertTrue(descriptor.isChooseFolders)
|
||||||
|
assertFalse(descriptor.isChooseFiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun panel(
|
||||||
|
choose: (JComponent) -> String? = { null },
|
||||||
|
input: (String, String) -> String? = { _, _ -> null },
|
||||||
|
edit: (SkillDto, Boolean) -> SkillEditDialogHandle = { _, _ -> FakeSkillDialog("# Plan\nUse steps") },
|
||||||
|
): SkillsSettingsUi {
|
||||||
|
install()
|
||||||
|
val panel = edt { SkillsSettingsUi(scope!!, DIR, choose, input, edit) }
|
||||||
|
ui = panel
|
||||||
|
edt { panel.reload(); true }
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun install() {
|
||||||
|
val cs = CoroutineScope(SupervisorJob())
|
||||||
|
scope = cs
|
||||||
|
appRpc = FakeAppRpcApi()
|
||||||
|
workspaceRpc = FakeWorkspaceRpcApi()
|
||||||
|
agentRpc = FakeAgentBehaviorRpcApi().apply {
|
||||||
|
skills = listOf(
|
||||||
|
SkillDto("plan", "Plan work", CUSTOM, "# Plan\nUse steps", editable = true),
|
||||||
|
SkillDto("thinking", "Built in", "builtin", "Built in content"),
|
||||||
|
SkillDto("remote", "Remote skill", REMOTE, "# Remote skill"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
app = KiloAppService(cs, appRpc)
|
||||||
|
val ready = KiloAppStateDto(
|
||||||
|
KiloAppStatusDto.READY,
|
||||||
|
config = ConfigDto(skills = SkillsConfigDto(
|
||||||
|
paths = listOf("/global/skills"),
|
||||||
|
urls = listOf("https://skills.test/base.json"),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
app._state.value = ready
|
||||||
|
appRpc.state.value = ready
|
||||||
|
ApplicationManager.getApplication().replaceService(KiloAppService::class.java, app, testRootDisposable)
|
||||||
|
ApplicationManager.getApplication().replaceService(KiloAgentBehaviorService::class.java, KiloAgentBehaviorService(cs, agentRpc), testRootDisposable)
|
||||||
|
ApplicationManager.getApplication().replaceService(KiloWorkspaceService::class.java, KiloWorkspaceService(cs, workspaceRpc), testRootDisposable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun click(list: JBList<SettingsListItem>, panel: SkillsSettingsUi, key: String, id: String) {
|
||||||
|
edt {
|
||||||
|
list.size = Dimension(520, 320)
|
||||||
|
list.doLayout()
|
||||||
|
val rows = if (list === skillsList(panel)) rows(panel) else sourceRows(panel)
|
||||||
|
val idx = rows.indexOfFirst { it.key == key }
|
||||||
|
list.selectedIndex = idx
|
||||||
|
val area = settingsListCellBounds(list, idx, selected = true).getValue(id)
|
||||||
|
click(list, center(area))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doubleClick(list: JBList<SettingsListItem>, panel: SkillsSettingsUi, key: String) {
|
||||||
|
edt {
|
||||||
|
list.size = Dimension(520, 320)
|
||||||
|
list.doLayout()
|
||||||
|
val idx = rows(panel).indexOfFirst { it.key == key }
|
||||||
|
list.selectedIndex = idx
|
||||||
|
val area = list.getCellBounds(idx, idx)
|
||||||
|
fire(list, mouse(list, MouseEvent.MOUSE_CLICKED, center(area), count = 2))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rows(panel: SkillsSettingsUi): List<SettingsListItem> = items(skillsList(panel))
|
||||||
|
|
||||||
|
private fun sourceRows(panel: SkillsSettingsUi): List<SettingsListItem> = items(sourceList(panel))
|
||||||
|
|
||||||
|
private fun items(list: JBList<SettingsListItem>): List<SettingsListItem> {
|
||||||
|
val model = list.model
|
||||||
|
return (0 until model.size).map { model.getElementAt(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun skillsList(panel: SkillsSettingsUi) = components(panel).filterIsInstance<JBList<SettingsListItem>>().first()
|
||||||
|
|
||||||
|
private fun sourceList(panel: SkillsSettingsUi) = components(panel).filterIsInstance<JBList<SettingsListItem>>().last()
|
||||||
|
|
||||||
|
private fun scrollFor(panel: SkillsSettingsUi, list: JBList<SettingsListItem>) = components(panel)
|
||||||
|
.filterIsInstance<JBScrollPane>()
|
||||||
|
.single { pane -> pane.viewport.view === list.parent }
|
||||||
|
|
||||||
|
private fun progressText(panel: SkillsSettingsUi) = components(panel.progress).filterIsInstance<JBLabel>().single().text
|
||||||
|
|
||||||
|
private fun SkillEditDialog.okText(): String {
|
||||||
|
val method = DialogWrapper::class.java.getDeclaredMethod("getOKAction")
|
||||||
|
method.isAccessible = true
|
||||||
|
return (method.invoke(this) as javax.swing.Action).getValue(javax.swing.Action.NAME) as String
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun components(root: java.awt.Component): List<java.awt.Component> {
|
||||||
|
val out = mutableListOf<java.awt.Component>()
|
||||||
|
fun visit(item: java.awt.Component) {
|
||||||
|
out += item
|
||||||
|
if (item is Container) item.components.forEach { visit(it) }
|
||||||
|
}
|
||||||
|
visit(root)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun layout(root: java.awt.Component) {
|
||||||
|
root.doLayout()
|
||||||
|
if (root is Container) root.components.filterIsInstance<Container>().forEach { layout(it) }
|
||||||
|
UIUtil.dispatchAllInvocationEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun center(rect: java.awt.Rectangle) = Point(rect.x + rect.width / 2, rect.y + rect.height / 2)
|
||||||
|
|
||||||
|
private fun click(list: JBList<SettingsListItem>, point: Point) {
|
||||||
|
fire(list, mouse(list, MouseEvent.MOUSE_PRESSED, point))
|
||||||
|
fire(list, mouse(list, MouseEvent.MOUSE_RELEASED, point))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mouse(list: JBList<SettingsListItem>, id: Int, point: Point, count: Int = 1) = MouseEvent(
|
||||||
|
list,
|
||||||
|
id,
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
if (id == MouseEvent.MOUSE_PRESSED) InputEvent.BUTTON1_DOWN_MASK else 0,
|
||||||
|
point.x,
|
||||||
|
point.y,
|
||||||
|
count,
|
||||||
|
false,
|
||||||
|
MouseEvent.BUTTON1,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun <T> edt(block: () -> T): T {
|
||||||
|
var result: T? = null
|
||||||
|
ApplicationManager.getApplication().invokeAndWait { result = block() }
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return result as T
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun flushUntil(done: () -> Boolean) = runBlocking {
|
||||||
|
repeat(300) {
|
||||||
|
delay(10)
|
||||||
|
edt { UIUtil.dispatchAllInvocationEvents(); true }
|
||||||
|
if (done()) return@runBlocking
|
||||||
|
}
|
||||||
|
edt { UIUtil.dispatchAllInvocationEvents(); true }
|
||||||
|
assertTrue(done())
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val DIR = "/test"
|
||||||
|
const val CUSTOM = "/home/test/.config/kilo/skill/plan/SKILL.md"
|
||||||
|
const val REMOTE = "/home/test/.cache/kilo/skills/remote/SKILL.md"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FakeSkillDialog(private val text: String, private val show: () -> Unit = {}) : SkillEditDialogHandle {
|
||||||
|
override fun showAndGet(): Boolean {
|
||||||
|
show()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
override fun content() = text
|
||||||
|
}
|
||||||
+60
@@ -88,6 +88,66 @@ class SettingsDraftStateTest {
|
|||||||
assertFalse(state.modified())
|
assertFalse(state.modified())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `stale external base after fallback completion does not revert applied target`() {
|
||||||
|
val state = SettingsDraftState("old")
|
||||||
|
state.update { "new" }
|
||||||
|
val token = state.start()!!
|
||||||
|
state.complete(token, "old")
|
||||||
|
|
||||||
|
state.accept("old")
|
||||||
|
|
||||||
|
assertEquals("new", state.baseline)
|
||||||
|
assertEquals("new", state.draft)
|
||||||
|
assertFalse(state.modified())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `stale external base after fresh completion does not revert applied target`() {
|
||||||
|
val state = SettingsDraftState("old")
|
||||||
|
state.update { "new" }
|
||||||
|
val token = state.start()!!
|
||||||
|
state.complete(token, "new")
|
||||||
|
|
||||||
|
state.accept("old")
|
||||||
|
|
||||||
|
assertEquals("new", state.baseline)
|
||||||
|
assertEquals("new", state.draft)
|
||||||
|
assertFalse(state.modified())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fresh external base after ignored stale update is accepted`() {
|
||||||
|
val state = SettingsDraftState("old")
|
||||||
|
state.update { "new" }
|
||||||
|
val token = state.start()!!
|
||||||
|
state.complete(token, "old")
|
||||||
|
state.accept("old")
|
||||||
|
|
||||||
|
state.accept("other")
|
||||||
|
|
||||||
|
assertEquals("other", state.baseline)
|
||||||
|
assertEquals("other", state.draft)
|
||||||
|
assertFalse(state.modified())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `older stale external base after multiple saves is ignored`() {
|
||||||
|
val state = SettingsDraftState("old")
|
||||||
|
state.update { "new" }
|
||||||
|
val first = state.start()!!
|
||||||
|
state.complete(first, "new")
|
||||||
|
state.update { "other" }
|
||||||
|
val second = state.start()!!
|
||||||
|
state.complete(second, "other")
|
||||||
|
|
||||||
|
state.accept("old")
|
||||||
|
|
||||||
|
assertEquals("other", state.baseline)
|
||||||
|
assertEquals("other", state.draft)
|
||||||
|
assertFalse(state.modified())
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `failed save keeps draft dirty and restores previous base`() {
|
fun `failed save keeps draft dirty and restores previous base`() {
|
||||||
val state = SettingsDraftState("old")
|
val state = SettingsDraftState("old")
|
||||||
|
|||||||
+33
@@ -14,6 +14,9 @@ import java.awt.Dimension
|
|||||||
import java.awt.Point
|
import java.awt.Point
|
||||||
import java.awt.event.InputEvent
|
import java.awt.event.InputEvent
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
|
import javax.swing.ListSelectionModel
|
||||||
|
import javax.swing.Scrollable
|
||||||
|
import javax.swing.SwingConstants
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
class SettingsListViewTest : BasePlatformTestCase() {
|
class SettingsListViewTest : BasePlatformTestCase() {
|
||||||
@@ -236,6 +239,25 @@ class SettingsListViewTest : BasePlatformTestCase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun `test action click invokes on second selected row in multi selection list`() {
|
||||||
|
edt {
|
||||||
|
val calls = mutableListOf<String>()
|
||||||
|
val cfg = SettingsListConfig.Equal.copy(selection = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
|
||||||
|
val view = SettingsListView("Empty", cfg) { key, id -> calls += "$key:$id" }
|
||||||
|
view.update(listOf(
|
||||||
|
item("a", "Alpha", null, SettingsListCell("edit", "Edit", alwaysVisible = false)),
|
||||||
|
item("b", "Beta", null, SettingsListCell("edit", "Edit", alwaysVisible = false)),
|
||||||
|
))
|
||||||
|
layout(view)
|
||||||
|
view.list.selectedIndices = intArrayOf(0, 1)
|
||||||
|
|
||||||
|
val area = settingsListCellBounds(view.list, 1, selected = true).getValue("edit")
|
||||||
|
click(view, center(area))
|
||||||
|
|
||||||
|
assertEquals(listOf("b:edit"), calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun `test update selects preferred key`() {
|
fun `test update selects preferred key`() {
|
||||||
edt {
|
edt {
|
||||||
val view = SettingsListView("Empty") { _, _ -> }
|
val view = SettingsListView("Empty") { _, _ -> }
|
||||||
@@ -260,6 +282,17 @@ class SettingsListViewTest : BasePlatformTestCase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun `test list view tracks viewport width`() {
|
||||||
|
edt {
|
||||||
|
val view = SettingsListView("Empty") { _, _ -> }
|
||||||
|
view.update(listOf(item("long", "Alpha", "A very long description that should wrap instead of scrolling")))
|
||||||
|
|
||||||
|
assertTrue((view as Scrollable).getScrollableTracksViewportWidth())
|
||||||
|
assertFalse(view.getScrollableTracksViewportHeight())
|
||||||
|
assertEquals(160, view.getScrollableBlockIncrement(java.awt.Rectangle(0, 0, 320, 160), SwingConstants.VERTICAL, 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun item(id: String, name: String, note: String?, vararg cells: SettingsListCell) = object : SettingsListItem {
|
private fun item(id: String, name: String, note: String?, vararg cells: SettingsListCell) = object : SettingsListItem {
|
||||||
override val key = id
|
override val key = id
|
||||||
override val title = name
|
override val title = name
|
||||||
|
|||||||
+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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
-2
@@ -11,9 +11,14 @@ import ai.kilocode.rpc.dto.SkillDto
|
|||||||
|
|
||||||
class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi {
|
class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi {
|
||||||
var agents = emptyList<AgentDetailDto>()
|
var agents = emptyList<AgentDetailDto>()
|
||||||
|
var skills = emptyList<SkillDto>()
|
||||||
var mcps = emptyList<McpStatusDto>()
|
var mcps = emptyList<McpStatusDto>()
|
||||||
var mcpConfigs = emptyMap<String, McpServerConfigDto>()
|
var mcpConfigs = emptyMap<String, McpServerConfigDto>()
|
||||||
val agentCalls = mutableListOf<String>()
|
val agentCalls = mutableListOf<String>()
|
||||||
|
val skillCalls = mutableListOf<String>()
|
||||||
|
val skillRemovals = mutableListOf<Pair<String, String>>()
|
||||||
|
val skillReloads = mutableListOf<String>()
|
||||||
|
val skillSaves = mutableListOf<Triple<String, String, String>>()
|
||||||
val mcpCalls = mutableListOf<String>()
|
val mcpCalls = mutableListOf<String>()
|
||||||
val mcpConfigCalls = mutableListOf<String>()
|
val mcpConfigCalls = mutableListOf<String>()
|
||||||
val mcpSaves = mutableListOf<Triple<String, String, McpConfigDto?>>()
|
val mcpSaves = mutableListOf<Triple<String, String, McpConfigDto?>>()
|
||||||
@@ -27,10 +32,16 @@ class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi {
|
|||||||
var afterRemove: (suspend (String, String) -> Unit)? = null
|
var afterRemove: (suspend (String, String) -> Unit)? = null
|
||||||
var afterMcpConnect: (suspend (String, String) -> Unit)? = null
|
var afterMcpConnect: (suspend (String, String) -> Unit)? = null
|
||||||
var createError: Exception? = null
|
var createError: Exception? = null
|
||||||
|
var skillsError: Exception? = null
|
||||||
var removeError: Exception? = null
|
var removeError: Exception? = null
|
||||||
|
var removeSkillError: Exception? = null
|
||||||
|
var saveSkillError: Exception? = null
|
||||||
var mcpStatusError: Exception? = null
|
var mcpStatusError: Exception? = null
|
||||||
var mcpConnectError: Exception? = null
|
var mcpConnectError: Exception? = null
|
||||||
var removeResult = true
|
var removeResult = true
|
||||||
|
var removeSkillResult = true
|
||||||
|
var reloadSkillResult = true
|
||||||
|
var saveSkillResult = true
|
||||||
var mcpConnectResult = true
|
var mcpConnectResult = true
|
||||||
var mcpDisconnectResult = true
|
var mcpDisconnectResult = true
|
||||||
var mcpAuthenticateResult = true
|
var mcpAuthenticateResult = true
|
||||||
@@ -43,12 +54,41 @@ class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi {
|
|||||||
|
|
||||||
override suspend fun skills(directory: String): List<SkillDto> {
|
override suspend fun skills(directory: String): List<SkillDto> {
|
||||||
assertNotEdt("agentBehavior.skills")
|
assertNotEdt("agentBehavior.skills")
|
||||||
return emptyList()
|
skillsError?.let { throw it }
|
||||||
|
skillCalls.add(directory)
|
||||||
|
return skills
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun removeSkill(directory: String, location: String): Boolean {
|
override suspend fun removeSkill(directory: String, location: String): Boolean {
|
||||||
assertNotEdt("agentBehavior.removeSkill")
|
assertNotEdt("agentBehavior.removeSkill")
|
||||||
return false
|
removeSkillError?.let { throw it }
|
||||||
|
skillRemovals.add(directory to location)
|
||||||
|
if (removeSkillResult) skills = skills.filterNot { it.location == location }
|
||||||
|
return removeSkillResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun reloadSkills(directory: String): Boolean {
|
||||||
|
assertNotEdt("agentBehavior.reloadSkills")
|
||||||
|
skillReloads.add(directory)
|
||||||
|
return reloadSkillResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun saveSkill(directory: String, location: String, content: String): Boolean {
|
||||||
|
assertNotEdt("agentBehavior.saveSkill")
|
||||||
|
saveSkillError?.let { throw it }
|
||||||
|
skillSaves.add(Triple(directory, location, content))
|
||||||
|
if (saveSkillResult) skills = skills.map { if (it.location == location) it.copy(content = content) else it }
|
||||||
|
return saveSkillResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun saveSkills(directory: String, edits: Map<String, String>): Boolean {
|
||||||
|
assertNotEdt("agentBehavior.saveSkills")
|
||||||
|
saveSkillError?.let { throw it }
|
||||||
|
for ((location, content) in edits) skillSaves.add(Triple(directory, location, content))
|
||||||
|
if (saveSkillResult) skills = skills.map { skill ->
|
||||||
|
edits[skill.location]?.let { skill.copy(content = it) } ?: skill
|
||||||
|
}
|
||||||
|
return saveSkillResult
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun removeAgent(directory: String, name: String): Boolean {
|
override suspend fun removeAgent(directory: String, name: String): Boolean {
|
||||||
|
|||||||
+24
@@ -2,6 +2,7 @@ package ai.kilocode.client.testing
|
|||||||
|
|
||||||
import ai.kilocode.rpc.KiloAppRpcApi
|
import ai.kilocode.rpc.KiloAppRpcApi
|
||||||
import ai.kilocode.rpc.dto.AgentConfigDto
|
import ai.kilocode.rpc.dto.AgentConfigDto
|
||||||
|
import ai.kilocode.rpc.dto.CompactionConfigDto
|
||||||
import ai.kilocode.rpc.dto.ConfigDto
|
import ai.kilocode.rpc.dto.ConfigDto
|
||||||
import ai.kilocode.rpc.dto.ConfigPatchDto
|
import ai.kilocode.rpc.dto.ConfigPatchDto
|
||||||
import ai.kilocode.rpc.dto.DeviceAuthDto
|
import ai.kilocode.rpc.dto.DeviceAuthDto
|
||||||
@@ -16,6 +17,7 @@ import ai.kilocode.rpc.dto.ModelVariantUpdateDto
|
|||||||
import ai.kilocode.rpc.dto.ProfileDto
|
import ai.kilocode.rpc.dto.ProfileDto
|
||||||
import ai.kilocode.rpc.dto.SkillsConfigDto
|
import ai.kilocode.rpc.dto.SkillsConfigDto
|
||||||
import ai.kilocode.rpc.dto.TelemetryCaptureDto
|
import ai.kilocode.rpc.dto.TelemetryCaptureDto
|
||||||
|
import ai.kilocode.rpc.dto.WatcherConfigDto
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -196,12 +198,34 @@ class FakeAppRpcApi : KiloAppRpcApi {
|
|||||||
val mcp = patch.mcp?.entries?.fold(config.mcp) { acc, (name, item) ->
|
val mcp = patch.mcp?.entries?.fold(config.mcp) { acc, (name, item) ->
|
||||||
if (item == null) acc - name else acc + (name to item)
|
if (item == null) acc - name else acc + (name to item)
|
||||||
} ?: config.mcp
|
} ?: config.mcp
|
||||||
|
val watcher = patch.watcher?.let { item ->
|
||||||
|
val cfg = config.watcher
|
||||||
|
cfg?.copy(ignore = item.ignore ?: cfg.ignore)
|
||||||
|
?: WatcherConfigDto(ignore = item.ignore ?: emptyList())
|
||||||
|
} ?: config.watcher
|
||||||
|
val compaction = patch.compaction?.let { item ->
|
||||||
|
val cfg = item.clear.fold(config.compaction ?: CompactionConfigDto()) { next, field ->
|
||||||
|
when (field) {
|
||||||
|
"threshold_percent" -> next.copy(threshold_percent = null)
|
||||||
|
"auto" -> next.copy(auto = null)
|
||||||
|
"prune" -> next.copy(prune = null)
|
||||||
|
else -> next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg.copy(
|
||||||
|
auto = item.auto ?: cfg.auto,
|
||||||
|
threshold_percent = item.threshold_percent ?: cfg.threshold_percent,
|
||||||
|
prune = item.prune ?: cfg.prune,
|
||||||
|
)
|
||||||
|
} ?: config.compaction
|
||||||
return config.copy(
|
return config.copy(
|
||||||
defaultAgent = if (values.containsKey("default_agent")) values["default_agent"] else config.defaultAgent,
|
defaultAgent = if (values.containsKey("default_agent")) values["default_agent"] else config.defaultAgent,
|
||||||
model = if (values.containsKey("model")) values["model"] else config.model,
|
model = if (values.containsKey("model")) values["model"] else config.model,
|
||||||
smallModel = if (values.containsKey("small_model")) values["small_model"] else config.smallModel,
|
smallModel = if (values.containsKey("small_model")) values["small_model"] else config.smallModel,
|
||||||
subagentModel = if (values.containsKey("subagent_model")) values["subagent_model"] else config.subagentModel,
|
subagentModel = if (values.containsKey("subagent_model")) values["subagent_model"] else config.subagentModel,
|
||||||
subagentVariant = if (values.containsKey("subagent_variant")) values["subagent_variant"] else config.subagentVariant,
|
subagentVariant = if (values.containsKey("subagent_variant")) values["subagent_variant"] else config.subagentVariant,
|
||||||
|
watcher = watcher,
|
||||||
|
compaction = compaction,
|
||||||
instructions = patch.instructions ?: config.instructions,
|
instructions = patch.instructions ?: config.instructions,
|
||||||
skills = patch.skills?.let { SkillsConfigDto(paths = it.paths.orEmpty(), urls = it.urls.orEmpty()) } ?: config.skills,
|
skills = patch.skills?.let { SkillsConfigDto(paths = it.paths.orEmpty(), urls = it.urls.orEmpty()) } ?: config.skills,
|
||||||
mcp = mcp,
|
mcp = mcp,
|
||||||
|
|||||||
+6
@@ -26,6 +26,12 @@ interface KiloAgentBehaviorRpcApi : RemoteApi<Unit> {
|
|||||||
|
|
||||||
suspend fun removeSkill(directory: String, location: String): Boolean
|
suspend fun removeSkill(directory: String, location: String): Boolean
|
||||||
|
|
||||||
|
suspend fun reloadSkills(directory: String): Boolean
|
||||||
|
|
||||||
|
suspend fun saveSkill(directory: String, location: String, content: String): Boolean
|
||||||
|
|
||||||
|
suspend fun saveSkills(directory: String, edits: Map<String, String>): Boolean
|
||||||
|
|
||||||
suspend fun removeAgent(directory: String, name: String): Boolean
|
suspend fun removeAgent(directory: String, name: String): Boolean
|
||||||
|
|
||||||
suspend fun createAgent(directory: String, input: AgentCreateDto): Boolean
|
suspend fun createAgent(directory: String, input: AgentCreateDto): Boolean
|
||||||
|
|||||||
+1
-1
@@ -48,7 +48,7 @@ interface KiloWorkspaceRpcApi : RemoteApi<Unit> {
|
|||||||
/** Resolve [path] to matching files, scoped primarily to [directory]. */
|
/** Resolve [path] to matching files, scoped primarily to [directory]. */
|
||||||
suspend fun files(directory: String, path: String): List<WorkspaceFileDto>
|
suspend fun files(directory: String, path: String): List<WorkspaceFileDto>
|
||||||
|
|
||||||
/** Fuzzy file/folder search via the backend IDE index. */
|
/** Fuzzy file/folder search via Kilo Core. */
|
||||||
suspend fun searchFiles(directory: String, query: String, limit: Int = 50): FileSearchResultDto
|
suspend fun searchFiles(directory: String, query: String, limit: Int = 50): FileSearchResultDto
|
||||||
|
|
||||||
/** Current uncommitted git changes as a unified diff for @git-changes mentions. */
|
/** Current uncommitted git changes as a unified diff for @git-changes mentions. */
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user