fix(jetbrains): remediate settings apply lifecycle

This commit is contained in:
kirillk
2026-07-01 16:23:37 -04:00
parent beca5698b6
commit fd3042c5f8
59 changed files with 554 additions and 2779 deletions
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Fix opening JetBrains Agent Behavior settings in frontend runs.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Support editing agent behavior settings from the JetBrains Agents settings page.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Support editing existing MCP servers from the JetBrains MCP settings page.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Use a wider agent ID field when creating agents in JetBrains settings and avoid refetching agents before the backend finishes reloading.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Make JetBrains settings list actions easier and more reliable to click.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Keep JetBrains settings Apply state consistent while asynchronous saves finish.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Keep JetBrains settings list rows at a consistent height when some entries do not have descriptions.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Fix MCP server environment variable edits in the JetBrains settings dialog.
-6
View File
@@ -1,6 +0,0 @@
---
"@kilocode/cli": patch
"@kilocode/kilo-jetbrains": patch
---
Prevent non-removable generated agents from showing a delete action in JetBrains settings and return a client error for expected agent removal failures.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Restrict built-in agent editing in JetBrains settings to safe overrides and support deleting custom agents.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": minor
---
Support exporting custom agents as `.agent.json` definitions from the agent edit dialog.
+2 -1
View File
@@ -1,5 +1,6 @@
---
"@kilocode/cli": patch
"@kilocode/kilo-jetbrains": patch
---
Stage agent create, import, and delete changes in JetBrains settings until Apply or OK.
Improve JetBrains agent, MCP, provider, and model settings so changes are staged until Apply, persist through the CLI, reload accurately, and hide unsupported removal actions.
@@ -1,107 +0,0 @@
# Settings List Mutation Refresh Plan
## Goal
Make JetBrains settings list mutations feel consistent and safe across agent behavior configurables:
- Show the standard settings progress overlay while a backend mutation is running or the CLI backend is reloading.
- Refresh the list only after the backend is ready again.
- Select the newly created row after add-agent when possible.
- Select a stable neighboring row after delete/removal.
- Reuse the behavior from the shared list base class instead of keeping add-agent-specific wait logic.
## Current Context
- `SettingsListPanel.reload()` already shows `loadingText()` via `SettingsProgressOverlay`, then calls `fetch()` and `view.update(items)`.
- Current add-agent code in `AgentsConfigurable.kt` has bespoke `waitForReload()` logic inside `CreateAction`.
- `SettingsListView.update(items)` preserves the current selected key, but has no public way to request a new preferred key or a previous index fallback.
- Agent delete currently updates local state optimistically after `removeAgent()` instead of waiting for the backend to reload and refetching.
- `SkillsSettingsUi` has both local draft removals and backend `removeSkill()` removals; only the backend removal should use the backend-ready mutation flow.
- `WorkflowsSettingsUi` has no mutations today, but should benefit from the base helper if mutations are added later.
- `McpSettingsUi` and `ProvidersSettingsUi` do not extend `SettingsListPanel`; do not migrate them in this change unless needed for compilation.
## Implementation Steps
1. Extend `SettingsListView` selection support.
- Add a way for `update` to accept an optional preferred key and/or preferred index.
- Keep existing behavior as the default: preserve the current selected key, then fall back to the first row.
- Add an EDT-only accessor for the current selected index if the base panel needs to capture it before a delete.
2. Extend `SettingsListPanel` reload internals.
- Change `reload()` to delegate to a private or protected reload implementation that accepts a selection target.
- Update the existing `apply(id, items)` path to pass selection information into `view.update(...)`.
- Keep manual refresh behavior unchanged apart from using the new shared implementation.
3. Add a reusable backend mutation helper to `SettingsListPanel`.
- Provide a protected EDT entrypoint such as `mutateAndReload(...)` that:
- Refuses to start when `busy` or disposed, using the existing `launch` gate.
- Shows `loadingText()` or an optional progress message through the existing progress overlay.
- Runs the supplied suspend mutation off EDT.
- If the mutation returns false, clears progress and busy state without refetching.
- Waits for a possible app reload by observing `KiloAppService.state`.
- Fetches fresh rows in the same launched job.
- Applies rows once on EDT with the requested selection target.
- Move the add-agent `READY -> non-READY -> READY` wait logic into this helper.
- Use the same timeouts as the existing add-agent fix unless tests show they need adjustment: short timeout to detect whether a reload starts, longer timeout to wait for readiness.
- Avoid calling public `reload()` from inside the helper because `launch` already marks the panel busy and nested `reload()` would be ignored.
4. Model selection targets in the base class.
- Support at least:
- Preserve current selection, for ordinary refresh.
- Prefer a specific key, for add-agent selecting the new agent.
- Prefer the previous selected index, for delete/removal selecting a neighboring row.
- Keep this internal to the settings base package if possible.
5. Update add-agent flow.
- Remove `AgentsSettingsUi.CreateAction.waitForReload()` and its local app-state imports/constants.
- Use the base `mutateAndReload` helper after `createAgent(dir, input)`.
- Request selection by the created agent name.
- Continue using the injectable `AgentCreateDialogHandle` seam from the existing tests.
6. Update delete-agent flow.
- Keep the confirmation dialog unchanged.
- Use the base `mutateAndReload` helper around `removeAgent(dir, agent.name)`.
- Request selection by previous index.
- Before the helper fetches fresh rows, ensure local agent draft/baseline state no longer contains the deleted agent so dirty draft merging in `fetch()` cannot resurrect it.
- Do not optimistically call `view.update(rows())`; the visible list should change when the refreshed data is applied.
7. Update skills backend removal.
- For discovered skills (`skill:` keys), use the base `mutateAndReload` helper around `removeSkill(dir, location)`.
- Request selection by previous index.
- Leave local `path:` and `url:` removals as immediate draft updates followed by local reload, since they do not perform backend mutations.
8. Keep workflows compatible.
- No behavior change is required for `WorkflowsSettingsUi` because it is read-only today.
- Confirm it still compiles with the updated base class.
9. Update user-visible strings only if needed.
- Prefer reusing `settings.agentBehavior.loading=Loading items...` for the progress overlay.
- Add a more specific message only if the implementation needs different text for mutation/reload waiting.
10. Update tests.
- Add or extend `SettingsListViewTest` to cover preferred-key and preferred-index selection after `update`.
- Update `AgentsSettingsUiTest` so add-agent asserts the `reviewer` row is selected after refresh.
- Update the add-agent backend-reload regression to assert the standard progress overlay remains visible while the app is `LOADING` and the row appears only after `READY`.
- Update delete-agent coverage to assert removal waits for backend readiness/refetch and selects a neighboring row.
- Add fake RPC support for delete-after hooks if needed, mirroring the existing `afterCreate` hook.
- Add skill-removal coverage if there is already a practical `SkillsSettingsUi` test fixture; otherwise keep it focused on the shared helper and agent flows.
11. Validate.
- Run focused tests from `packages/kilo-jetbrains/`:
- `./gradlew :frontend:test --tests "ai.kilocode.client.settings.base.SettingsListViewTest" --tests "ai.kilocode.client.settings.agents.AgentCreateDialogTest" --tests "ai.kilocode.client.settings.agents.AgentsSettingsUiTest"`
- Include any new skills settings test class in the same command if added.
- Run `./gradlew typecheck` from `packages/kilo-jetbrains/`.
## Risks And Safeguards
- Dirty agent drafts can reintroduce a deleted agent during refetch. Safeguard by pruning deleted agents from local draft/baseline state before refetch applies.
- A generic helper can accidentally delay local-only changes. Safeguard by using it only for backend mutations, not for pure draft edits.
- Nested reload calls will be ignored while `busy` is true. Safeguard by fetching and applying inside the mutation helper's own launched job.
- If the app leaves `READY` and never returns, the helper should not silently hang forever. Use the existing timeout pattern and surface the failure through the existing `launch` error/progress handling.
- All Swing reads/writes must remain on EDT. Capture selection and update the view only on EDT; run RPC and waiting off EDT.
## Out Of Scope
- Migrating `ProvidersSettingsUi` or `McpSettingsUi` to `SettingsListPanel`.
- Changing backend RPC contracts.
- Rebuilding or regenerating CLI/SDK artifacts.
@@ -1,91 +0,0 @@
# JetBrains Session Error Logging Plan
## Context
The `maple-squirrel` branch currently adds logging for normal `ChatEventDto.Error` events on the backend chat route, backend RPC route, frontend client route, and prompt acceptance path. A read-only audit found that explicit `session.error` DTOs are now visible once parsed, but several edge cases can still silently miss or under-report session errors.
Affected package: `packages/kilo-jetbrains/`.
## Decisions
- Keep the PR focused on diagnostic logging only.
- Do not reintroduce the previously reverted session-error footer UI work.
- Treat any chat event that carries an error payload as error-bearing for logging purposes.
- Preserve content-safety behavior by using existing `ChatLogSummary` preview controls and body summaries rather than logging full payloads by default.
- Prefer WARN logs for actual error-bearing events and abnormal flow termination; keep normal subscription lifecycle at INFO.
## Implementation Tasks
1. Add a shared event helper in `ChatLogSummary` or a nearby logging utility.
- Identify error-bearing events: `ChatEventDto.Error` and `ChatEventDto.MessageUpdated` where `event.info.error != null`.
- Add a summary that includes `sid`, event type, error type, optional status code, and previewed message via existing preview controls.
- Avoid exposing full response bodies unless existing chat-content preview/full logging is enabled.
2. Update backend chat event logging in `KiloBackendChatManager.start`.
- WARN-log every error-bearing parsed event before emitting it to `_events`.
- Include `route=chat-events`, `emit=true`, raw SSE type, byte count, and current subscriber count if available.
- This catches errors even when there are no frontend/RPC subscribers.
3. Make backend event normalization failure-safe.
- Wrap `normalizer.parse(event.type, event.data)` in `runCatching` inside the SSE collector.
- On failure, log WARN with event type, byte count, body summary/hash, and exception.
- Continue collecting subsequent SSE events after parse failures.
- Keep the existing `parse returned null` warning, but make it clear when the null came from `session.error` or another chat event type.
4. Update backend RPC flow logging in `KiloSessionRpcApiImpl.events`.
- WARN-log error-bearing events that pass the session filter.
- Use `onCompletion { cause -> ... }`.
- Log INFO for normal completion or cancellation.
- Log WARN with the cause for non-cancellation failures.
5. Update frontend client flow logging in `KiloSessionService.events`.
- WARN-log error-bearing events received from RPC.
- Use `onCompletion { cause -> ... }`.
- Log INFO for normal completion or cancellation.
- Log WARN with the cause for non-cancellation failures.
6. Update frontend controller subscription logging in `SessionController.subscribeEvents`.
- Add a catch path around event collection that logs WARN with `sid`, route/controller context, and exception.
- Keep the existing final unsubscribe/debug lifecycle log.
- Do not change UI state behavior unless a current state change already exists for that failure path.
7. Consider child session subscription logging in `SessionController.subscribeChild`.
- Add the same WARN-on-collection-failure pattern for child permission subscriptions.
- Keep this diagnostic-only unless it would alter visible behavior.
8. Improve SSE transport failure detail in `KiloBackendConnectionService.onFailure`.
- Pass the throwable to `log.warn(..., t)` when present.
- Log response code and body summary when `response` is present.
- Avoid consuming large response bodies beyond the existing safe summary behavior.
9. Add targeted tests where practical.
- Parser/chat manager test: malformed `session.error` SSE does not kill the watcher and logs a warning.
- Logging helper test: `MessageUpdated.info.error` is classified as error-bearing.
- Flow completion test, if lightweight test seams exist, for abnormal completion logging.
- Do not add broad UI tests unless production behavior changes.
## Validation
- Run `./gradlew typecheck` from `packages/kilo-jetbrains/`.
- Run the smallest relevant JetBrains tests if new tests are added.
- Inspect `git diff main...HEAD` and confirm the PR remains limited to logging/diagnostic files.
- Reproduce with JetBrains custom debug category `#ai.kilocode:all:separate` and verify logs show:
- prompt accepted,
- subscription start,
- backend chat error-bearing event WARN,
- RPC/client error-bearing event WARN when subscribed,
- parse failures as WARN without stopping later events,
- abnormal flow completion as WARN with cause.
## Risks
- WARN logs may become noisy if `MessageUpdated.info.error` repeats during streaming. Mitigate by summarizing and, if necessary, logging only when the error identity changes per message ID.
- Subscriber-count logging may require accessing `MutableSharedFlow.subscriptionCount`; keep that localized to `KiloBackendChatManager` where the mutable flow is owned.
- Response bodies can contain sensitive provider details. Use existing preview/full controls and safe body summaries.
## Out Of Scope
- Showing session errors in the UI footer.
- Changing retry, auth, or model-selection behavior.
- Replaying/caching global errors for UI delivery. This plan only makes missed delivery diagnosable.
- CLI/server changes outside `packages/kilo-jetbrains/`.
@@ -1,99 +0,0 @@
# JetBrains Settings List Renderer Layout Plan
## Goal
Fix shared JetBrains settings-list rendering so each settings page can choose the correct row-height behavior and row text spacing:
- Providers use each row's own preferred height.
- Agents, MCPs, skills, and workflows keep equal row heights.
- Row title/header text has no extra renderer padding.
- Row descriptions, when present, get a slight left padding so they read as secondary text under the title.
- MCP rows stay visually aligned with agent rows.
## Current Context
- Shared list rendering lives in `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/`:
- `SettingsListView.kt`
- `SettingsListRenderer.kt`
- `SettingsListModel.kt`
- `SettingsListPanel.kt`
- `SettingsListView.syncCellHeight(...)` currently always computes the tallest rendered row and assigns it to `JBList.fixedCellHeight`.
- This equal-height behavior works for agents/MCP-style lists but makes provider rows overly tall because providers have sections, icons, descriptions, and selected action cells.
- Providers currently use `SettingsListView` through `ProvidersContent` in `ProvidersSettingsUi.kt`, so they inherit equal-height behavior unintentionally.
- Agent Behavior pages use `SettingsListPanel`, so base-class configuration is the right place to preserve equal heights consistently.
## Decisions
1. Add an explicit shared settings-list layout/config model in base settings list code.
2. Make row-height behavior configurable:
- Equal tallest row: current behavior; used by agents, MCPs, skills, workflows.
- Preferred row height: set `fixedCellHeight = -1`; used by providers.
3. Make text inset behavior configurable in the shared renderer:
- Title/header line gets no extra renderer padding.
- Description line gets a small left padding only when visible.
4. Configure every settings-list consumer explicitly instead of relying on hidden defaults.
5. Keep changes in base settings classes plus page configuration only; do not change individual row data to fake layout.
## Implementation Tasks
1. Update `SettingsListModel.kt` or a nearby base file with shared list layout types.
- Add an enum/sealed type for row height policy, for example `SettingsListRowHeight.Equal` and `SettingsListRowHeight.Preferred`.
- Add a small config data class, for example `SettingsListConfig`, containing row height policy and row text spacing options.
- Keep names short and consistent with repo style.
2. Update `SettingsListView.kt`.
- Accept a `SettingsListConfig` constructor parameter.
- Pass the config to `SettingsListRenderer`.
- In `syncCellHeight(rows)`, preserve current max-height calculation for equal-height mode.
- In preferred-height mode, set `list.fixedCellHeight = -1` and revalidate only if it changed.
- Keep recalculation on `update(...)` and `filter(...)`.
- Ensure action hit testing still uses actual `getCellBounds(...)` from the list.
3. Update `SettingsListRenderer.kt`.
- Accept the shared config.
- Remove shared internal left padding from the title/header area.
- Keep only the real icon-to-title gap when an icon exists.
- Apply slight left padding to the description component only when description text is visible.
- Preserve right-side action alignment and badge behavior.
- Avoid hardcoded raw Swing dimensions where `UiStyle.Gap` or `JBUI` helpers apply.
4. Update `SettingsListPanel.kt`.
- Accept/pass a list config into its internal `SettingsListView`.
- Default can remain equal-height for safety, but all current subclasses should pass the intended config explicitly.
5. Configure all consumers.
- `ProvidersContent` in `ProvidersSettingsUi.kt`: use preferred row height and the shared row text spacing.
- `AgentsSettingsUi` in `AgentsConfigurable.kt`: use equal row height.
- `McpSettingsUi` in `McpConfigurable.kt`: use equal row height and same base layout as agents.
- `SkillsSettingsUi` in `SkillsConfigurable.kt`: use equal row height.
- `WorkflowsSettingsUi` in `WorkflowsConfigurable.kt`: use equal row height.
6. Avoid unrelated changes.
- Do not touch unrelated unstaged agent import work or localized bundles unless tests/typecheck require a direct import/string fix.
- Do not rewrite provider logic, MCP loading, or agent behavior data flow.
## Testing Tasks
1. Update `SettingsListViewTest.kt`.
- Keep existing tests proving equal-height behavior remains the default or explicit equal mode.
- Add a preferred-height test where a row with description is taller than a plain row.
- Assert preferred-height mode leaves `fixedCellHeight == -1`.
- Add/adjust a test proving filtering respects the configured row-height mode.
2. Add renderer spacing coverage in base tests where practical.
- Render a row with title and description.
- Assert title starts without extra renderer left padding relative to the content area.
- Assert description has a small left offset only when visible.
3. Update `ProvidersSettingsUiTest.kt`.
- Verify provider content/list uses preferred row heights.
- Use a provider row with description/section and a simpler row, then assert their rendered bounds are not forced equal.
- Keep existing provider renderer action-label and icon/description tests passing.
4. Update or add focused assertions in `AgentsSettingsUiTest.kt` and `McpSettingsUiTest.kt`.
- Assert rows with and without descriptions keep equal heights.
- Assert MCP action/title alignment remains consistent with the agent list behavior.
## Validation
Run focused JetBrains frontend checks from `packages/kilo-jetbrains/`:
```sh
./gradlew :frontend:test --tests ai.kilocode.client.settings.base.SettingsListViewTest --tests ai.kilocode.client.settings.providers.ProvidersSettingsUiTest --tests ai.kilocode.client.settings.agents.AgentsSettingsUiTest --tests ai.kilocode.client.settings.agents.McpSettingsUiTest
./gradlew :frontend:compileKotlin
```
If the Gradle test filter is not accepted by the current setup, run the nearest equivalent `:frontend:test` invocation that includes those test classes.
## Risks And Edge Cases
- Variable provider row heights with selected-only action cells may need `revalidate()` on selection changes if selected actions increase preferred height. Prefer keeping selected action cells within the unselected row height; only add selection revalidation if a test or manual inspection shows clipping.
- Section headers are part of rendered row preferred height. In preferred provider mode, only section-start rows should include that extra height.
- Equal-height lists should continue measuring rows as selected so selected-only action cells do not clip.
- Removing title/header padding should not remove icon-to-title spacing; icon rows still need a visible gap between icon and title.
- Description padding should not reserve space when no description exists.
@@ -1,371 +0,0 @@
# JetBrains "Agent Behavior" settings parity
Bring the VS Code extension's **Agent Behaviour** settings tab to the JetBrains plugin with
**full feature parity**, built on the existing CLI-ready settings infrastructure and reusing
(and extending) the common settings classes.
## Decisions (from clarification)
- **Structure:** Separate IntelliJ Settings tree nodes per sub-tab (not one page with internal sub-tabs).
- **Scope:** Full end-to-end parity — agent create/edit/import/export, per-agent permission editor,
calculated permissions, MCP runtime connect/disconnect/auth, skills discovery + on-disk removal,
workflows viewer, instruction files, skill paths/urls.
- **Marketplace:** Do **not** wire up browsing/installing. Render a **disabled "Browse Marketplace"
button with a "Coming soon" tooltip/note** everywhere VS Code shows it (Agents, MCP, Skills).
- **Claude Code compatibility:** Full parity — add a JetBrains plugin setting wired into the CLI spawn env.
## Source of truth (VS Code, for verbatim behavior/strings)
- `packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx` (5 sub-tabs)
- `ModeCreateView.tsx`, `ModeEditView.tsx`, `McpEditView.tsx`, `PermissionEditor.tsx`,
`permission-utils.ts`, `mode-io.ts`, `agent-behaviour/WorkflowsTab.tsx`
- Strings: `packages/kilo-vscode/webview-ui/src/i18n/en.ts` lines 13171469 (`settings.agentBehaviour.*`,
`settings.autoApprove.*`). Reuse the English copy verbatim.
Everything in `packages/kilo-jetbrains/` is Kilo-owned, so **no `kilocode_change` markers** are needed.
---
## Target tree structure
Today (registered in `frontend/src/main/resources/kilo.jetbrains.frontend.xml`, lines 2655):
```
Tools → Kilo Code (KiloSettingsConfigurable, id ...settings)
├── Models (...settings.models, groupWeight 1)
├── Providers (...settings.providers, groupWeight 1)
└── Profile (...settings.profile, groupWeight 2)
```
Add a grouped **Agent Behavior** node with five children (mirrors the existing root-with-nav pattern
in `KiloSettingsConfigurable`):
```
Tools → Kilo Code
├── Models
├── Providers
├── Agent Behavior (NEW group node, ...settings.agentBehavior, groupWeight 1)
│ ├── Agents (...settings.agentBehavior.agents)
│ ├── MCP Servers (...settings.agentBehavior.mcp)
│ ├── Rules (...settings.agentBehavior.rules)
│ ├── Workflows (...settings.agentBehavior.workflows)
│ └── Skills (...settings.agentBehavior.skills)
└── Profile
```
- The **Agent Behavior** node itself is a plain `SearchableConfigurable` like `KiloSettingsConfigurable`
(`frontend/.../settings/KiloSettingsConfigurable.kt`): a short description + `ActionLink`s to the five
children. It does **not** gate on CLI ready and holds no settings.
- The five children are real configurables (gated on CLI ready). Register each as
`<applicationConfigurable parentId="ai.kilocode.jetbrains.settings.agentBehavior" ...>` in
`kilo.jetbrains.frontend.xml`. (Per the JetBrains AGENTS.md, configurables go in the module XML, not
root `plugin.xml`.)
> Alternative considered: five flat siblings directly under Kilo Code (no group node). The group node is
> chosen to preserve the "Agent Behavior" concept while still being separate tree nodes.
---
## Architecture: layers to change
All paths under `packages/kilo-jetbrains/`.
### 1. Shared DTOs (`shared/.../rpc/dto/`)
**Read model — extend `ConfigDto`** (`KiloAppStateDto.kt`, currently only model fields):
- `ConfigDto` += `defaultAgent: String?`, `instructions: List<String>`, `skills: SkillsConfigDto?`,
`mcp: Map<String, McpConfigDto>`.
- `AgentConfigDto` += `prompt`, `description`, `mode`, `hidden`, `disable`, `temperature`, `top_p`,
`steps`, `permission: PermissionConfigDto?` (keep existing `model`, `variant`).
- New DTOs: `SkillsConfigDto(paths, urls)`, `McpConfigDto(type, command: List<String>?, url, environment: Map<String,String>?)`,
`PermissionConfigDto = Map<String, PermissionRuleDto>`, and
`PermissionRuleDto` = sealed { `Level(value: String?)` | `Patterns(map: Map<String,String?>)` } (mirrors
`PermissionRule` in `permissions.ts`).
**Write model — extend `ConfigPatchDto`** (currently `values: Map<String,String?>` + `agents: Map<String,AgentConfigPatchDto{model}>`):
- Keep `values` for scalars (add `default_agent` to the allowlist).
- Add `instructions: List<String>?` (null = no change; empty list = clear), `skills: SkillsPatchDto?`,
`mcp: Map<String, McpConfigDto?>?` (entry present = upsert; value null = delete server).
- Expand `AgentConfigPatchDto` to the **full** agent shape: `model`, `variant`, `prompt`, `description`,
`mode`, `hidden`, `disable`, `temperature`, `top_p`, `steps`, `permission: PermissionConfigDto?`.
**Patch semantics (matches the CLI's deep-merge + null-as-delete):** the frontend includes an
agent/mcp object in the patch only when that entity changed vs baseline, and sends the **full target
object** with explicit `null` for cleared scalar fields (exactly how VS Code's `ModeEditView`/`McpEditView`
accumulate into the draft, e.g. sending explicit `false`/`null` so the CLI overwrites/deletes). This avoids
a per-field "present vs null" wrapper.
### 2. New RPC: `KiloAgentBehaviorRpcApi` (directory-scoped reads + runtime actions)
The config-backed values come from `KiloAppService.state.config` (existing). The **workspace/runtime**
data and side-effecting actions need a new RPC (modeled on `KiloWorkspaceRpcApi`):
- `shared/.../rpc/KiloAgentBehaviorRpcApi.kt` (`@Rpc`, `RemoteApi<Unit>`, suspend methods) with DTOs:
- `agents(directory): List<AgentDetailDto>` — name, displayName, description, mode, native, hidden,
deprecated, and **resolved permission rules** (`List<PermissionRuleItemDto>`).
- `skills(directory): List<SkillDto{name, description, location}>`
- `removeSkill(directory, location): Boolean`
- `removeAgent(directory, name): Boolean`
- `commands(directory): List<CommandDto{name, description, template}>` (workflows)
- `mcpStatus(directory): List<McpStatusDto{name, status, error?}>`
- `mcpConnect(directory, name)`, `mcpDisconnect(directory, name)`
- `mcpAuthenticate(directory, name)` (+ `mcpAuthStart`/`mcpAuthCallback` if OAuth code flow is needed)
- Backend impl `backend/.../rpc/KiloAgentBehaviorRpcApiImpl.kt` delegates to the **already-generated**
`DefaultApi` client (`backend/build/generated/openapi/.../client/DefaultApi.kt`), which exposes typed
methods for every endpoint: `appAgents`, `appSkills`, `kilocodeRemoveSkill`, `kilocodeRemoveAgent`,
`commandList`, `mcpStatus`, `mcpConnect`, `mcpDisconnect`, `mcpAuthAuthenticate`, etc. Map results to DTOs.
- Register a provider `KiloAgentBehaviorRpcApiProvider` in
`backend/src/main/resources/kilo.jetbrains.backend.xml` (mirror `KiloAppRpcApiProvider`, lines 913) and
in the shared module descriptor. Calls require `app.requireReady()`.
- Frontend service `frontend/.../app/KiloAgentBehaviorService.kt` (`@Service(APP)`) wraps the RPC with
`durable {}` like `KiloWorkspaceService`.
> Underlying CLI routes (for reference): `GET /agent`, `GET /skill`(v2)/`app.skills`, `GET /command`,
> `GET /mcp` + `POST /mcp/{name}/connect|disconnect|auth/authenticate`,
> `POST /kilocode/skill/remove`, `POST /kilocode/agent/remove`. The "calculated permissions" come from the
> `Agent.permission` field on `GET /agent` (no separate endpoint).
### 3. Config write extension (backend)
`backend/.../cli/KiloCliDataParser.kt``buildConfigPatch` (lines 626649) currently allowlists 4 keys
and emits strings only. Extend to emit JSON for:
- `default_agent` (string|null) — add to allowlist.
- `instructions` (string array).
- `skills: { paths: [...], urls: [...] }`.
- `mcp: { name: {…} | null }` (null deletes).
- `agent: { name: { full agent object incl. booleans/numbers/prompt/permission } }`.
- Permission objects: serialize `PermissionConfigDto`/`PermissionRuleDto` (level string or pattern map,
null deletes a key).
Add unit tests in `backend/src/test/.../cli/KiloCliDataParserTest.kt` (the existing model-patch tests are
around lines 16571692) covering each new field type, null-as-delete, and full-agent objects.
The write path itself is unchanged: `KiloAppService.updateConfig(patch)``KiloAppRpcApi.updateConfig`
`KiloBackendAppService.updateConfig``PATCH /global/config`.
### 4. Backend read mapping
`backend/.../rpc/KiloAppRpcApiImpl.kt``config(c: Config)` (lines 176203) maps the generated `Config`
(which already deserializes `command`, `skills`, `defaultAgent`, `agent`, `mcp`, `instructions`,
`permission`) into the extended `ConfigDto`. Extend the `agents()` and `agent()` mappers to populate the
new `AgentConfigDto` fields incl. permission.
---
## New / extended common settings classes
Reuse: `KiloReadyConfigurable`, `BaseSettingsUi`, `BaseContentPanel.section()`, `SettingsRow`/`SettingsRows`,
`SettingsPanel`, `SettingsTop` banners, `SettingsProgressOverlay`, `UiStyle.*`, `Stack`/`Align`.
Extract these **new common classes** into `frontend/.../settings/base/` (or `client/ui/` where generic):
1. **`SettingsToggle`** — reusable boolean control for the `SettingsRow` value slot. Wrap
`com.intellij.ui.components.OnOffButton` (switch, to match VS Code's `Switch`) with
`selected`/`onToggle`. (The research confirmed no settings toggle widget exists yet.) This is the key
new common class.
2. **`SettingsListEditor`** — the add-field + removable-rows pattern shared by Instruction Files, Skill
Paths, Skill URLs, and MCP env/args. A `JBTextField` + "Add" button + rows (monospace value + close
icon), optional per-row "open file" (pencil) action; `onChange(List<String>)` callback. Built from
`SettingsRows` + `JBUI` spacing.
3. **`SettingsSectionHeader`** — section title with right-aligned action buttons (Available Agents +
Import / Browse Marketplace / Create New Mode). Complements `BaseContentPanel.section()`.
4. **`comingSoonButton(text)`** — disabled `JButton` with a "Coming soon" tooltip, used for every
"Browse Marketplace" placement. Add to `UiStyle.Components` (or `settings/base`).
5. **`SettingsBadge`** — small tag label (custom / subagent / hidden / disabled / deprecated) using theme
colors (`JBUI.CurrentTheme` badge colors or `SimpleColoredComponent`), no hardcoded hex.
6. **`SettingsNavigator`** — `CardLayout` master/detail container with back navigation, for the Agents and
MCP list↔edit/create drill-down (replaces VS Code's per-tab `view` signal).
7. **`LevelSelect`** — Default/Allow/Ask/Deny dropdown (`JComboBox`) used by the permission editor
(Default = inherit).
Feature-specific (not "common") helpers, under `frontend/.../settings/agentbehavior/`:
8. **`PermissionUtils.kt`** — Kotlin port of `permission-utils.ts` (wildcard/exception patch builders,
`effectiveRuleLevel`, `mostRestrictive`, `inheritedWildcard`, `permissionExceptions`).
9. **`PermissionEditorPanel`** — port of `PermissionEditor.tsx`: granular tools (external_directory, bash,
read, edit with wildcard + add-path/add-command exceptions), simple tools, grouped (todoread/todowrite),
trailing (websearch/webfetch/doom_loop); each row a `LevelSelect`; emits `PermissionConfigDto` patches.
10. **`CalculatedPermissionsPanel`** — read-only collapsible table (Tool / Pattern / Action), effective
wildcard summary chips, copy-as-JSON; data from `AgentDetailDto.permission`.
---
## Per-node implementation
Each child configurable extends `KiloReadyConfigurable` (CLI-ready gate) and resolves the open project's
`basePath` for the directory (like `ModelsConfigurable.kt` line 17). Config-backed editing uses
`BaseSettingsUi<…>` (draft → IntelliJ **Apply**/**Reset**), and workspace/runtime data is fetched in
`loadWorkspace()` via `KiloAgentBehaviorService` (mirroring `ModelsSettingsUi.loadWorkspace → workspaces.models`).
### A. Agents (`AgentsConfigurable` + `AgentsSettingsUi`)
- **List view** (`SettingsNavigator` master):
- **Default Agent** `SettingsRow` + dropdown → config `default_agent`. Options: visible primary agents +
"Default" (empty → null). Reuse a combo like Models' picker pattern.
- **`SettingsSectionHeader`** "Available Agents" with `[Import]`, `comingSoonButton("Browse Marketplace")`,
`[Create New Mode]`.
- Agent rows: name + `SettingsBadge`s (custom/subagent/hidden/disabled/deprecated), 0.5 opacity when
disabled, remove (custom only) + edit chevron. Empty state "No agents found.".
- **Create view** (`ModeCreateView` parity): name (validated `^[a-z][a-z0-9-]*$`, unique), description,
system prompt → adds `agent[slug] = {mode:"primary", description, prompt}` to draft.
- **Edit view** (`ModeEditView` parity): description (custom only), system prompt / prompt-override (native
shows the "built-in mode" note), Model Override + Variant Override (reuse `ModelSettingPicker` /
`ReasoningPicker`), Temperature, Top P, Max Steps (`JBTextField` parse), Hidden + Disabled
(`SettingsToggle`; setting true clears `default_agent` if it points here), **`PermissionEditorPanel`**
(custom agents) writing `agent.<name>.permission`, and **`CalculatedPermissionsPanel`** (read-only).
- **Import** (`mode-io.ts` port): IntelliJ `FileChooser` → parse/validate JSON (≤1 MB) → merge into draft;
surface `nameRequired`/`nameInvalid`/`nameTaken`/`invalidJson`/`tooLarge` errors via `SettingsTop` banner.
- **Export** (custom): `FileSaverDialog` → write `{name}.agent.json`.
- **Remove** (custom): confirm dialog → **immediate** `KiloAgentBehaviorService.removeAgent` (deletes the
custom agent file; matches `kilocode.removeAgent`), then reload agents.
- Config edits commit via **Apply**; add/remove agent + default_agent are part of the draft except the
file-deleting Remove which is immediate (parity with VS Code).
### B. MCP Servers (`McpConfigurable` + `McpSettingsUi`)
- Header `comingSoonButton("Browse Marketplace")`.
- List from `ConfigDto.mcp` joined with runtime `mcpStatus` (service): status dot + label, expandable detail
(command/args/url/env), **connect/disconnect `SettingsToggle`** (immediate RPC), **Sign In** button when
`needs_auth` (immediate `mcpAuthenticate`), remove (immediate config write removing `mcp.<name>`), edit
chevron. Empty state string. Status refreshed on load and after each action.
- **Edit view** (`McpEditView` parity): transport note; local → Command + Arguments (`SettingsListEditor`,
one arg/line) + Environment Variables (KEY/value add + rows); remote → Server URL. Edits go to the draft
**Apply** (writes `mcp.<name>`).
### C. Rules (`RulesConfigurable` + `RulesSettingsUi`)
- Section description (`rules.description`).
- **Additional Instruction Files**: `SettingsListEditor` (add path, per-row pencil "open file" via
`KiloWorkspaceService.openPath`, remove) → config `instructions` (draft → Apply).
- **Claude Code Compatibility** section: `SettingsRow` + `SettingsToggle` "Load Claude Code Files"
(description incl. "Requires restart"). This is a **plugin setting**, not CLI config — see below.
### D. Workflows (`WorkflowsConfigurable`)
- Read-only. Reuse `SettingsPanel` + `section()`. Description (`workflows.description`). List from
`KiloAgentBehaviorService.commands`: `/name` + description, expandable Description/Template. Empty state.
- No draft; can use `KiloReadyConfigurable` directly with a small ready panel that calls the service on
ready (no `BaseSettingsUi` needed since `isModified` is always false).
### E. Skills (`SkillsConfigurable` + `SkillsSettingsUi`)
- Header `comingSoonButton("Browse Marketplace")`.
- **Discovered Skills**: list from `KiloAgentBehaviorService.skills`; non-builtin rows get a remove →
confirm dialog → **immediate** `removeSkill(location)` (deletes files on disk), then reload. Empty state.
- **Skill Folder Paths** + **Skill URLs**: two `SettingsListEditor`s → config `skills.paths` / `skills.urls`
(draft → Apply).
---
## Claude Code compatibility (full parity)
VS Code: `kilo-code.new.claudeCodeCompat` (default false). The backend launcher sets
`KILO_DISABLE_CLAUDE_CODE: "true"` **only when compat is false** (`server-manager.ts` line 142). So enabling
compat = do not disable Claude Code in the spawned CLI.
JetBrains today always sets `KILO_DISABLE_CLAUDE_CODE=true` in
`backend/.../cli/KiloBackendCliManager.kt` (`buildKiloCliEnv`, line 306).
Plan:
- Add an app-level plugin setting `claudeCodeCompat` (default false). Store via a `PersistentStateComponent`
service (or extend `KiloPluginSettings`/`PropertiesComponent`). Must be readable from the **backend**
spawn path — choose an app-level `@Service` accessible in `backend` (place the setting service so both the
Rules UI in `frontend` and `KiloBackendCliManager` in `backend` can read/write it; if cross-module access
is awkward, expose read/write via the new `KiloAgentBehaviorRpcApi`).
- In `buildKiloCliEnv`, make `KILO_DISABLE_CLAUDE_CODE` conditional: set `"true"` only when compat is
**false** (matches VS Code exactly).
- The Rules toggle writes the setting **immediately** and prompts/triggers a CLI restart
(`KiloAppService.restart()` exists) since it only takes effect on respawn ("Requires restart").
---
## i18n strings
Add `settings.agentBehavior.*` keys to `frontend/src/main/resources/messages/KiloBundle.properties`
(reuse the VS Code English copy verbatim from `en.ts` 13171437), plus:
- Display names: `settings.agentBehavior.displayName`, `.agents.displayName`, `.mcp.displayName`,
`.rules.displayName`, `.workflows.displayName`, `.skills.displayName`.
- `save.pending` / `save.failed` per node (mirror `settings.models.save.*`).
- The permission editor reuses `settings.autoApprove.*` (levels, tool descriptions, add path/command,
exceptions) — add those keys too.
Add matching entries to the 18 `KiloBundle_<locale>.properties` files (English fallback is acceptable
initially; translation is a follow-up).
---
## File-by-file change list
**Shared (`shared/src/main/kotlin/ai/kilocode/rpc/`)**
- `dto/KiloAppStateDto.kt` — extend `ConfigDto`, `AgentConfigDto`, `ConfigPatchDto`, `AgentConfigPatchDto`;
add `SkillsConfigDto`, `McpConfigDto`, `SkillsPatchDto`, `PermissionConfigDto`, `PermissionRuleDto`.
- `dto/AgentBehaviorDto.kt` (new) — `AgentDetailDto`, `SkillDto`, `CommandDto`, `McpStatusDto`,
`PermissionRuleItemDto`.
- `KiloAgentBehaviorRpcApi.kt` (new).
- shared module descriptor — register the new RPC if needed.
**Backend (`backend/src/main/kotlin/ai/kilocode/backend/`)**
- `rpc/KiloAppRpcApiImpl.kt` — extend `config()`/`agents()`/`agent()` mappers.
- `cli/KiloCliDataParser.kt` — extend `buildConfigPatch` for the new fields + permission JSON.
- `rpc/KiloAgentBehaviorRpcApiImpl.kt` (new) + `rpc/KiloAgentBehaviorRpcApiProvider.kt` (new).
- `cli/KiloBackendCliManager.kt` — conditional `KILO_DISABLE_CLAUDE_CODE` based on the compat setting.
- claude-compat setting service (new, app-level, backend-readable).
- `src/main/resources/kilo.jetbrains.backend.xml` — register the new RPC provider.
**Frontend (`frontend/src/main/kotlin/ai/kilocode/client/`)**
- `app/KiloAgentBehaviorService.kt` (new).
- `settings/base/SettingsToggle.kt`, `SettingsListEditor.kt`, `SettingsSectionHeader.kt`,
`SettingsBadge.kt`, `SettingsNavigator.kt`, `LevelSelect.kt` (new common classes);
`comingSoonButton` in `ui/UiStyle.kt` (`Components`).
- `settings/AgentBehaviorConfigurable.kt` (new group node, nav links).
- `settings/agentbehavior/` (new): `AgentsConfigurable`/`AgentsSettingsUi`(+state),
`McpConfigurable`/`McpSettingsUi`, `RulesConfigurable`/`RulesSettingsUi`,
`WorkflowsConfigurable`, `SkillsConfigurable`/`SkillsSettingsUi`,
`PermissionUtils.kt`, `PermissionEditorPanel.kt`, `CalculatedPermissionsPanel.kt`,
`ModeIo.kt` (import/export port).
- `settings/KiloSettingsConfigurable.kt` — add an `ActionLink` to Agent Behavior (optional, mirrors lines 4264).
- `src/main/resources/kilo.jetbrains.frontend.xml` — register the group node + 5 child configurables.
- `messages/KiloBundle*.properties` — new strings.
**Tests (`*/src/test/...`)**
- `KiloCliDataParserTest` — new patch encodings (arrays, bools, numbers, mcp, full agent, permission, null delete).
- Frontend settings tests extending `BasePlatformTestCase` (mirror `KiloReadyConfigurableTest`,
`SettingsRow`/state tests): list editor add/remove, toggle, permission patch builders (`PermissionUtils`),
agent draft → patch, navigator master/detail, calculated permissions rendering.
- `KiloBackendCliManagerEnvTest``KILO_DISABLE_CLAUDE_CODE` flips with the compat setting.
---
## Suggested build sequence
1. **Foundation:** shared DTO extensions + `buildConfigPatch` extension + backend `config()` mapper + tests.
2. **Common classes:** `SettingsToggle`, `SettingsListEditor`, `SettingsSectionHeader`, `SettingsBadge`,
`comingSoonButton`, `LevelSelect`, `SettingsNavigator`.
3. **RPC + service:** `KiloAgentBehaviorRpcApi`(+impl/provider/registration) + `KiloAgentBehaviorService`.
4. **Group node + registration** in `kilo.jetbrains.frontend.xml` + bundle display names.
5. **Rules** (instruction files + Claude compat incl. CLI-manager env wiring) — smallest config+plugin-setting node.
6. **Skills** (paths/urls config + discovered list + removal).
7. **Workflows** (read-only).
8. **MCP Servers** (config edit + runtime connect/auth/remove).
9. **Agents** (list/create/edit + `PermissionUtils`/`PermissionEditorPanel`/`CalculatedPermissionsPanel` +
import/export) — largest, do last.
10. Full strings sweep, locale files, and verification.
## Verification
From `packages/kilo-jetbrains/`: `bun run typecheck` (or `./gradlew typecheck`) and `./gradlew test`
(requires Java 21). Manual: `./gradlew runIde`, open Settings → Tools → Kilo Code → Agent Behavior, verify
each node gates on CLI ready and matches VS Code behavior; confirm the disabled "Browse Marketplace
(coming soon)" buttons; toggle Claude Code compat and confirm CLI respawns with/without
`KILO_DISABLE_CLAUDE_CODE`.
## Risks / notes
- **Patch semantics**: full-object-per-changed-entity relies on the CLI's deep-merge + null-as-delete (as
VS Code does). Cover with `KiloCliDataParserTest` and a manual round-trip check.
- **Permission editor** is the most intricate port (`PermissionEditor.tsx` + `permission-utils.ts`);
isolate in `PermissionUtils.kt` with unit tests before wiring UI.
- **MCP OAuth**: `mcpAuthAuthenticate` may need the start/callback code flow for some servers; scope the
authenticate path to match VS Code's `McpOAuth` (connect → if needs_auth, authenticate).
- **Claude-compat setting placement**: must be readable from `backend` at spawn time; if a frontend-only
store is used, expose it to the backend (e.g. via the new RPC or a shared app service) so
`KiloBackendCliManager` can read it.
- **Generated `DefaultApi`** already covers all needed endpoints; no SDK/CLI regeneration required for the
reads/actions. Only the JetBrains plugin (shared/backend/frontend) changes.
-159
View File
@@ -1,159 +0,0 @@
# JetBrains Agent Edit Dialog
## Goal
Implement the Agents settings `Edit` action as a modal `DialogWrapper` that edits a parent-owned draft model. Dialog `OK` saves into the settings page draft only; JetBrains Settings `Apply` / `OK` persists to CLI config, and `Reset` discards the draft.
## Scope
- Open a `DialogWrapper` from `AgentsSettingsUi.onCell(..., EDIT_CELL)`.
- Build the dialog with existing Swing settings helpers: `BaseContentPanel`, `SettingsRows`, `SettingsRow`, `SettingsToggle`, `Stack`, and IntelliJ platform components.
- Validate with IntelliJ platform validation (`initValidation()`, `doValidate()` / `doValidateAll()`, `ValidationInfo`, optionally `ComponentValidator` for live field feedback).
- Preserve regular `Configurable` workflow: edit dialog commit updates `AgentsSettingsUi` draft; `AgentBehaviorConfigurableBase.applyReady()` calls `applyDraft()`; `resetReady()` calls `resetDraft()`.
- Do not implement Add/Create/Import in this pass; leave existing placeholder actions.
- Do not add opencode server routes or SDK generation; ordinary agent settings can be persisted through existing `KiloAppService.updateConfig(ConfigPatchDto)`.
## Design Decisions
- Use a child `DialogWrapper`, not inline editing, so `Esc` only affects the edit dialog and cannot close the parent Settings dialog while editing.
- Treat dialog `OK` as "save to draft", not "write to disk". This is the key part that keeps JetBrains Settings Apply/Cancel/Reset semantics intact.
- Existing agent names remain read-only. Rename/create/delete are separate workflows because the agent map key is the identity.
- Start with the VS Code edit-mode fields that are config-backed and practical in this dialog: description, prompt, model override, variant, temperature, top_p, steps, hidden, disabled, mode.
- Display resolved/calculated permission rules read-only if useful, but do not build the full per-agent permission editor in this pass unless time remains. A raw JSON permission editor is a fallback only if explicitly desired during implementation.
## Patch DTO Fix First
Before editing arbitrary agent fields, fix the current `AgentConfigPatchDto` semantics.
Problem:
- `KiloCliDataParser.buildConfigPatch()` currently always emits `"model": null` for every `AgentConfigPatchDto` whose `model` property is null.
- That is safe for the existing model-settings use case, but unsafe for agent editing: a description-only patch would accidentally clear the model override.
- Nullable fields such as `prompt`, `description`, `variant`, `temperature`, `top_p`, and `steps` also cannot distinguish "unchanged" from "clear this field".
Plan:
- Extend `AgentConfigPatchDto` in `shared/src/main/kotlin/ai/kilocode/rpc/dto/KiloAppStateDto.kt` with a field such as `clear: List<String> = emptyList()`.
- Change `KiloCliDataParser.buildConfigPatch()` so agent fields are emitted only when non-null, and fields listed in `clear` are emitted as JSON null.
- Update `ModelsSettingsState.patch(...)` to use `AgentConfigPatchDto(clear = listOf("model"))` when clearing a per-agent model override.
- Update `FakeAppRpcApi.applyPatch(...)` to apply non-null agent fields and respect `clear` for tests.
- Update `KiloCliDataParserTest` expectations for per-agent model clear and add tests proving description-only patches do not emit `model: null`.
## Agent Draft Model
Add a focused state helper under `frontend/src/main/kotlin/ai/kilocode/client/settings/agentbehavior/`, likely `AgentSettingsState.kt`.
Data shape:
- `AgentsDraft(defaultAgent: String?, agents: Map<String, AgentEditDraft>)`.
- `AgentEditDraft` contains the editable config-backed fields plus display-only metadata needed for rows.
- Keep a `base` draft and mutable `draft` in `AgentsSettingsUi`.
Functions:
- `agentsDraft(config: ConfigDto?, details: List<AgentDetailDto>): AgentsDraft` merges `KiloAppService.state.value.config` with `KiloAgentBehaviorService.agents(dir)` results.
- `patch(from: AgentsDraft, to: AgentsDraft): ConfigPatchDto?` emits `values["default_agent"]` and `agents[name]` only for changed fields.
- `savedMatches(base, draft)` should compare only known draft fields, similar to `ModelsSettingsState.savedMatches(...)`.
- When `hidden` or `disable` becomes true for the current default agent, clear `defaultAgent` in the draft.
Notes:
- `AgentDetailDto` currently lacks some fields (`model`, `variant`, `prompt`, numeric overrides), so use `ConfigDto.agent[name]` for persisted override values.
- `AgentDetailDto` still provides row metadata and resolved permission rules.
- Built-in/native agents can accept config overrides; custom-only behavior should be limited to fields VS Code treats as custom-only, such as editing the description if needed.
## UI Changes
Modify `AgentsConfigurable.kt` or split new classes into nearby files:
- Store latest fetched `AgentDetailDto` list in `AgentsSettingsUi`.
- Build list rows from `draft` plus details so edited values and disabled/hidden badges update immediately after dialog OK.
- Implement `onCell(key, EDIT_CELL)`:
- Find the agent detail and current `AgentEditDraft`.
- Construct `AgentEditDialog(agent, draft, names)`.
- If `showAndGet()` returns true, update `draft` with dialog result and refresh list UI without writing config.
- Keep `DELETE_CELL` behavior unchanged unless the implementation needs a small guard; deletion remains outside this request.
- Update `modified()` to compare the whole `AgentsDraft` to base.
- Update `applyDraft()` to call `KiloAppService.updateConfig(patch(base, draft))`, then refresh base from returned config or fall back to the applied draft.
- Update `resetDraft()` to restore `draft = base`, picker selection, and list rows.
Dialog implementation:
- Add `AgentEditDialog : DialogWrapper(true)`.
- Constructor sets title, calls `init()`, then `initValidation()`.
- `createCenterPanel()` returns a `BaseContentPanel` with sections/rows.
- Use IntelliJ components: `JBTextField`, `JBTextArea` inside `JBScrollPane`, `JComboBox`, `SettingsToggle` / `JBCheckBox` if appropriate.
- Override `getPreferredFocusedComponent()` and `getDimensionServiceKey()`.
- Use `doValidateAll()` if multiple validation messages are useful, otherwise `doValidate()` for the first invalid field.
- Override `doCancelAction(...)` only if the dialog needs a discard confirmation for dirty dialog fields. Since dialog changes are not committed until OK, plain Cancel/Escape already forgets edits; a confirmation can be added if the UX should explicitly warn.
Validation rules:
- `mode` must be one of `primary`, `subagent`, `all`.
- `temperature` and `top_p` must be blank or finite numbers, preferably in `[0, 1]` for `top_p`; keep `temperature` finite and non-negative unless existing product behavior requires broader values.
- `steps` must be blank or a positive integer.
- `model` and `variant` are optional text fields in this pass; trim whitespace and treat blank as clear.
- If permission JSON editing is included, parse with `kotlinx.serialization.json.Json` and validate action values are `ask`, `allow`, `deny`, or null.
## Strings
Add user-facing strings to `frontend/src/main/resources/messages/KiloBundle.properties` for:
- Dialog title and section headings.
- Field titles/descriptions/placeholders.
- Validation messages.
- Save/Cancel labels only if default `DialogWrapper` labels are not sufficient.
- Saving/failed text for agent settings apply progress if surfaced.
If the repo convention requires locale bundles to contain every key, add English fallback values to the `KiloBundle_*.properties` files; otherwise rely on ResourceBundle fallback to the base bundle.
## Tests
Add focused tests rather than broad UI snapshots:
- `AgentSettingsStateTest`:
- Builds draft from config plus agent details.
- Emits default-agent changes.
- Emits changed description/prompt/mode/hidden/disable/numeric fields without emitting unrelated fields.
- Emits explicit clears via `clear`.
- Clears `defaultAgent` when hiding/disabling the selected default.
- `KiloCliDataParserTest`:
- Agent description-only patch does not emit `model: null`.
- Agent model clear emits `model: null` through `clear`.
- Full agent patch still emits booleans/numbers/permission correctly.
- `AgentsSettingsUiTest` if practical:
- Dialog OK updates draft and `modified()` becomes true without calling RPC update immediately.
- `applyDraft()` sends one `ConfigPatchDto` through `FakeAppRpcApi`.
- `resetDraft()` discards dialog changes.
- `AgentEditDialogTest` if practical:
- `performValidateAll()` reports invalid numeric fields on EDT.
- Cancel leaves parent draft unchanged by testing the parent `onCell` path or dialog result flow.
## Verification
Run the smallest relevant checks after implementation:
- `./gradlew typecheck` or `bun run typecheck` from `packages/kilo-jetbrains/`.
- Targeted frontend tests for new state/dialog tests if available.
- Targeted backend parser test if `KiloCliDataParserTest` is changed.
- Do not run `java -version` unless Gradle fails with a Java-version or missing-Java error.
## Files Expected To Change
- `packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/KiloAppStateDto.kt`
- `packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsState.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agentbehavior/AgentsConfigurable.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agentbehavior/AgentSettingsState.kt` or equivalent new helper
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agentbehavior/AgentEditDialog.kt` or equivalent new helper
- `packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties`
- Tests under `packages/kilo-jetbrains/frontend/src/test/...` and `packages/kilo-jetbrains/backend/src/test/...`
- A patch changeset under `.changeset/` because this is a user-facing JetBrains settings feature
## Out Of Scope
- Full Create Agent flow.
- Import/export agent JSON.
- Full visual per-agent permission editor parity with VS Code.
- Editing custom agent markdown files directly.
- New CLI HTTP endpoints or SDK regeneration.
@@ -1,97 +0,0 @@
# Plan: JetBrains Agent Editor Native Restrictions
## Goal
Update the JetBrains agent settings editor so custom agents remain editable, while built-in/native agents are intentionally restricted. Skip permissions editing for this pass.
## Current Findings
- JetBrains currently opens the same `AgentEditDialog` for native and custom agents.
- `AgentEditDialog.kt` currently allows all agents to edit `mode`, `hidden`, `disable`, description, prompt, model, variant, temperature, top P, and steps.
- `AgentSettingsState.kt` already preserves `native` on `AgentEditDraft`, but the UI and patch logic do not use it for restrictions.
- Custom-agent delete UI is shown, but `AgentsConfigurable.onCell()` only handles edit, so delete is currently non-functional.
- The backend/RPC already provides `native` metadata in `AgentDetailDto`; no JetBrains API changes are needed for this editor change.
- Permissions are present in DTOs but intentionally out of scope for this plan.
## Editing Matrix
| Field / Action | Custom Agent | Native Agent |
|---|---|---|
| Agent ID / name | Read-only, display only | Read-only, display only |
| Native identity | Not editable | Not editable |
| Delete / remove | Allowed once delete is wired | Not allowed |
| Mode (`primary` / `subagent` / `all`) | Editable | Read-only/disabled |
| Disable agent | Editable | Read-only/disabled |
| Hide agent | Editable | Read-only/disabled |
| Description | Editable | Editable safe override |
| Prompt | Editable | Editable safe override |
| Model | Editable | Editable safe override |
| Variant | Editable | Editable safe override |
| Temperature | Editable | Editable safe override |
| Top P | Editable | Editable safe override |
| Steps | Editable | Editable safe override |
| Permissions | Skip in this pass | Skip in this pass |
## Implementation Steps
1. Add capability helpers in `AgentSettingsState.kt` or near the UI model:
- `canDelete(agent)` returns `!agent.native`.
- `canEditMode(agent)` returns `!agent.native`.
- `canEditVisibility(agent)` returns `!agent.native` for `hidden` and `disable`.
- Keep permissions helpers out of this pass, or add only if needed as a private placeholder with no UI usage.
2. Defensively enforce restrictions in the state/patch layer:
- Update `updateAgent()` so native agents cannot clear the default agent by changing restricted fields from the UI.
- Update `patchAgent()` so native-agent changes to `mode`, `hidden`, and `disable` are ignored even if a dialog/model bug tries to send them.
- Keep safe override patching for native agents: `model`, `variant`, `prompt`, `description`, `temperature`, `top_p`, and `steps`.
- Keep custom-agent behavior unchanged for all currently supported fields.
3. Update `AgentEditDialog.kt` UI behavior:
- Leave the mode row visible for native agents, but disable the mode combo box and add explanatory text such as `Built-in agents cannot be changed to subagents.`
- Leave visibility rows visible for native agents, but disable `Hidden` and `Disabled` toggles and add explanatory text such as `Built-in agents cannot be hidden or disabled.`
- Keep name as display-only for every agent.
- For `ask`, use ask-specific safety copy only in the explanation if useful, for example `Ask is a built-in read-only primary agent.` Do not use the name for the general restriction logic.
- Do not add permissions UI in this pass.
4. Improve `SettingsToggle` only if needed:
- Prefer setting `isEnabled = false` on the existing toggle instance in `AgentEditDialog`.
- If this becomes repetitive, minimally extend `SettingsToggle` with an optional `enabled` parameter, but avoid broad changes to unrelated settings pages.
5. Update list behavior in `AgentsConfigurable.kt`:
- Use `canDelete(agent)` for whether delete is present/enabled.
- Wire `DELETE_CELL` for custom agents only if remove is considered currently supported by existing backend pieces.
- Implementation path: confirmation dialog, call `KiloAgentBehaviorService.removeAgent(dir, name)`, update draft/base rows after success, and clear `defaultAgent` if the deleted custom agent was selected.
- Native agents should not show delete, or should show it disabled only if a clear explanation can be surfaced; hiding is acceptable because the built-in badge already explains the distinction and the current list-cell model has no disabled reason text.
6. Add or update localized strings in `KiloBundle.properties`:
- Native mode restriction text.
- Native visibility restriction text.
- Optional ask-specific read-only/safety note.
- Delete confirmation title/message if wiring delete.
- For non-English bundles, either add English fallback values consistently with existing project practice or update only the base bundle if localized fallback is accepted in this repo.
7. Update tests in `AgentSettingsStateTest.kt`:
- Change tests that currently assume `code` can emit native-like `mode`, `hidden`, and `disable` changes.
- Add a custom-agent test proving `mode`, `hidden`, and `disable` still patch normally when `native = false`.
- Add a native-agent test proving `mode`, `hidden`, and `disable` changes are ignored in `patchAgent()` / `updateAgent()`.
- Add a native-agent default-agent test proving restricted native changes do not remove the selected default agent.
- Add a native draft merge test proving `native = true` is preserved from `AgentDetailDto`.
8. Optional UI tests, only if lightweight existing patterns support it:
- Instantiate `AgentEditDialog` with a native draft and verify mode/hidden/disabled controls are disabled.
- Instantiate with a custom draft and verify those controls are enabled.
- If dialog tests are not already practical in this package, rely on state tests plus manual UI verification.
## Backend/API Assumptions
- JetBrains receives `native` for agents returned by `/app/agents`; this is already mapped into `AgentDetailDto.native`.
- This plan does not change CLI/server behavior. The CLI currently still honors manually edited config overrides like `agent.ask.mode` or `agent.ask.disable`; this UI fix prevents JetBrains from creating those invalid edits going forward.
- If full hardening against manually edited config is required, that should be a separate backend/CLI task because it affects all clients and may need shared opencode-file annotations.
## Verification
Run the smallest relevant JetBrains checks after implementation:
- From `packages/kilo-jetbrains/`: `./gradlew typecheck`.
- From `packages/kilo-jetbrains/`: targeted frontend tests covering agent settings state, or the package test task if targeted Gradle filtering is not straightforward.
## Expected Result
- Custom agents remain editable as before, including mode and visibility fields.
- Custom agents can be deleted if the existing remove RPC path is wired in this pass.
- Native agents remain visible and editable only for safe overrides.
- Native agents cannot be changed to subagents, hidden, disabled, or removed through the JetBrains UI.
- The ask agent stays a native primary read-only/safe built-in from the JetBrains editor perspective.
- Permissions editing remains unimplemented for now.
@@ -1,216 +0,0 @@
# JetBrains Agent Settings — Add/Delete + End-to-End Test Coverage
Two parts:
- **Part A (feature)**: implement **adding** a custom agent in the JetBrains **Agents** settings page
(currently a no-op placeholder), reusing the CLI's existing agent-builder endpoint.
- **Part B (tests)**: end-to-end coverage for the full agent settings round trip — **load** (CLI → UI),
**save/edit** (UI → CLI), **add**, and **delete** — using **real frontend services backed by fake RPC
apis** (no mocks) and the **real Swing component tree**, matching the existing `ProvidersSettingsUiTest`
and `SessionControllerTestBase` patterns.
Delete is already implemented end-to-end; this plan keeps it and adds its tests.
## Decisions (confirmed with user)
- **Implement add.** Wire the existing "New Agent…" placeholder to a real create flow backed by the
CLI's agent-builder `save` endpoint.
- **Edit-property → CLI** stays covered by two cooperating tests (dialog form binding + existing state
transforms) rather than a test-only seam in the final, modal `AgentsSettingsUi`.
- Pure/validatable logic (state transforms, create validation) lives in plain functions that are unit
tested directly, so modal `DialogWrapper` flows never need to be driven headlessly.
## How agent create/delete work in the CLI (already present)
- **Create**: `PUT /agent-builder/:id``AgentBuilder.save` (`packages/opencode/src/kilocode/agent/builder.ts`)
writes a canonical agent markdown file (`<dir>/.kilo/agent/<id>.md` for project scope, `<config>/agent/<id>.md`
for global), then the handler disposes the instance store so agent state refreshes. Body fields:
`id`, `scope` (project|global), `mode` (primary|subagent|all), `description?`, `model?`, `color?`,
`steps?`, `tools?`, `permission?`, `prompt` (**required, non-blank**). The JetBrains generated client
**already exposes** `DefaultApi.agentBuilderSave(id, directory, workspace, AgentBuilderSaveRequest)`
and `AgentBuilderSaveRequest`**no SDK/CLI regen needed**.
- **Delete**: `POST /kilocode/agent/remove``KiloAgent.remove` deletes the agent markdown file (rejects
native/organization agents) and refreshes. Already wired through `removeAgent` RPC.
- Backend RPC impls (`KiloAgentBehaviorRpcApiImpl`) call the typed generated client (e.g. `api.appAgents`)
for reads and a raw okhttp `post(...)` for `removeAgent`. New `createAgent` uses the typed
`api.agentBuilderSave(...)` (mirrors `agents()`), so no new okhttp plumbing.
## Background / current data flow (UI)
- `AgentsSettingsUi` (`settings/agents/AgentsConfigurable.kt`) extends `SettingsListPanel` and implements
`AgentBehaviorPage`. It is `internal` and **final**, with `protected onCell`/`view`, so tests drive it
via its public API (`modified()`, `applyDraft()`, `resetDraft()`, `reload()`, `dispose()`) plus
component-tree traversal.
- **Load**: `fetch()` reads `KiloAgentBehaviorService.agents(dir)` + `KiloAppService.state.value.config`
+ `KiloWorkspaceService.models(dir).providers`, runs `agentsDraft(...)` (`AgentSettingsState.kt`) to merge
them, and renders `rows()` into the `SettingsListView` `JBList<SettingsListItem>`.
- **Save (edit/default agent)**: `applyDraft()``patch(base, draft)``KiloAppService.updateConfig(patch)`
→ RPC. `FakeAppRpcApi.updateConfig` records the patch and applies it to its state, returning new state —
a faithful CLI round-trip. The UI reloads the returned config into `base` (so `modified()``false`).
- **Edit properties**: `onCell(EDIT_CELL)` → modal `AgentEditDialog.showAndGet()``result()`
`updateAgent(draft, result)`. The modal `DialogWrapper` can't be accepted headlessly, so the dialog's
form binding is tested directly.
- **Add**: today `addAction()` builds a `DefaultActionGroup` of two `PlaceholderAction`s (Create/Import)
whose `actionPerformed` is a no-op. Part A replaces "Create" with a real action.
- **Delete**: `onCell(DELETE_CELL)``Messages.showYesNoDialog` (auto-answerable via `TestDialogManager`)
`KiloAgentBehaviorService.removeAgent(dir, name)` → RPC (`FakeAgentBehaviorRpcApi.removals`).
- **Default agent**: a `JComboBox<String>` built in `toolbarRight()` (real, non-modal).
- **Existing coverage**: `AgentSettingsStateTest` already unit-tests the transforms (`agentsDraft`,
`updateAgent`, `patch`, `savedMatches`). The gap is the *wiring* through real services + real Swing.
## Part A — Implement "Add agent"
All changes are in Kilo-owned packages (`kilo-jetbrains` + reuse of existing `opencode` endpoints); no
`packages/opencode/` files are modified.
1. **Shared DTO**`shared/.../rpc/dto/AgentBehaviorDto.kt`: add
`@Serializable data class AgentCreateDto(name, prompt, mode = MODE_PRIMARY, description? = null, scope = "project")`
(strings keep the wire payload simple; values validated before send).
2. **Shared RPC**`shared/.../rpc/KiloAgentBehaviorRpcApi.kt`: add
`suspend fun createAgent(directory: String, input: AgentCreateDto): Boolean`.
3. **Backend impl**`backend/.../rpc/KiloAgentBehaviorRpcApiImpl.kt`: implement `createAgent` by mapping
`AgentCreateDto``AgentBuilderSaveRequest` (`id = name`, `scope`, `mode`, `description`, `prompt`) and
calling `api.agentBuilderSave(id = input.name, directory = directory, agentBuilderSaveRequest = req)`;
return `true`. Mirrors the existing `agents()` typed-client call. The save handler refreshes state.
4. **Frontend service**`frontend/.../app/KiloAgentBehaviorService.kt`: add
`suspend fun createAgent(directory, input) = safe(false) { call { createAgent(directory, input) } }`.
5. **Pure create logic**`frontend/.../settings/agents/AgentCreateState.kt` (new, mirrors
`AgentSettingsState.kt`): `validateAgentCreate(input, existingNames): List<ValidationError>` (or a
message map) enforcing the agent-id regex `^[a-zA-Z0-9][a-zA-Z0-9._-]*$`, non-blank/non-duplicate name,
non-blank prompt, and valid mode. Unit-testable without a dialog.
6. **Create dialog**`frontend/.../settings/agents/AgentCreateDialog.kt` (new `DialogWrapper`): collects
name (`JBTextField`), prompt (`EditorTextField`/`JBTextArea`), mode (`ComboBox`), scope
(Project/Global `ComboBox`), description (`JBTextArea`, optional). `result(): AgentCreateDto`.
`doValidateAll()` delegates to `validateAgentCreate(result(), existingNames)`.
7. **Wire the action**`frontend/.../settings/agents/AgentsConfigurable.kt`: replace the Create
`PlaceholderAction` with a real `DumbAwareAction` that opens `AgentCreateDialog(draft.agents.keys)`, and
on OK runs `cs.launch { if (service<KiloAgentBehaviorService>().createAgent(dir, dialog.result())) withContext(edt) { reload() } }`.
Keep "Import" as a placeholder (out of scope). After `reload()`, the new agent (now on disk / in the
fake) renders in the list.
8. **i18n**`frontend/src/main/resources/messages/KiloBundle.properties`: add create-dialog keys
(`settings.agentBehavior.agents.create.title`, `.name`, `.name.invalid`, `.name.duplicate`, `.prompt`,
`.prompt.invalid`, `.mode`, `.scope`, `.scope.project`, `.scope.global`, `.description`). English source
only; other locales fall back.
9. **Test fake**`frontend/src/test/.../testing/FakeAgentBehaviorRpcApi.kt`: add a `createAgent` override
that records `creations` and appends a corresponding `AgentDetailDto(name, mode, native=false, ...)` to
`agents`, so a subsequent `agents()` / `reload()` surfaces it.
### Add UX note (consistent with edit)
The Create action opens a modal `DialogWrapper`, which cannot be accepted in a headless test, and
`AgentsSettingsUi` is final. So "click Create → dialog → CLI" is **not** driven end-to-end headlessly.
Coverage is split: dialog form binding (`AgentCreateDialogTest`), validation (`AgentCreateStateTest`),
service round trip (`KiloAgentBehaviorServiceTest`), and the panel rendering a created agent after
`reload()` (`AgentsSettingsUiTest`).
## Part B — Tests
All under `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/...`. Extend
`BasePlatformTestCase`; never mock the EDT/threading; settle background RPC with a `flushUntil` loop that
drains coroutines + the EDT.
### `settings/agents/AgentCreateStateTest.kt` (new)
- Valid input passes; blank name, regex-invalid name, duplicate name (vs `existingNames`), blank prompt,
and invalid mode each produce the expected validation error. Pure-function test, no UI.
### `settings/agents/AgentCreateDialogTest.kt` (new)
- Build on EDT (`edt { AgentCreateDialog(existingNames) }`); traverse `dialog.rootPane`, locating fields by
their `SettingsRow` title (same `KiloBundle.message(...)` keys the dialog uses).
- Default form is empty/`primary`/`project`. Set name, prompt, mode, scope, description on the real
components; `result()` returns the expected `AgentCreateDto`. Dispose via `edt { Disposer.dispose(dialog.disposable) }`.
### `settings/agents/AgentEditDialogTest.kt` (new) — *edit properties (form ⇄ data)*
- Real `AgentEditDialog`. Construct with `KiloAppService(scope, FakeAppRpcApi())` + `List<ModelPicker.Item>`.
- **Loads agent into form**: a fully-populated `AgentEditDraft` → assert each component shows the value
(description/prompt/model/variant/mode/temperature/topP/steps/hidden/disable).
- **Reads edits back**: mutate real components (`JBTextArea.text`, `EditorTextField.text`,
`JComboBox.selectedItem`, numeric `JBTextField.text`, toggles via `OnOffButton.doClick()`); assert
`result()` reflects every edit (blank→null/trim, numbers parsed, flags flipped, mode updated).
- **Native restrictions**: `native = true` → description non-editable, mode/visibility toggles disabled,
and `result()` leaves restricted fields unchanged.
- Locate fields via `SettingsRow`/`SettingsStackedRow` title labels; dispose the dialog in teardown.
### `app/KiloAgentBehaviorServiceTest.kt` (new) — *add + delete service round trips*
- `createAgent` forwards the right `AgentCreateDto` to the fake RPC and returns `true`; failure path
returns `false` (the `safe(...)` fallback).
- `removeAgent` returns `true` and records the removal; failure path returns `false`.
- Construct the service directly with `KiloAgentBehaviorService(scope, FakeAgentBehaviorRpcApi())` (matches
`KiloWorkspaceServiceTest`), call off the EDT.
### `settings/agents/AgentsSettingsUiTest.kt` (new) — *panel-level CLI ⇄ UI*
`installServices(...)`: `scope = CoroutineScope(SupervisorJob())`; build `app = KiloAppService(scope, appRpc)`
and seed `app._state.value = KiloAppStateDto(READY, config = ConfigDto(defaultAgent, agent))`;
`agentRpc.agents = listOf(AgentDetailDto(...))`; `workspaceRpc.models = ModelsWorkspaceDto(providers = ...)`
(provider id `kilo` so models survive the `items()` filter). Register all three on the application via
`replaceService(..., testRootDisposable)`. `ui = edt { AgentsSettingsUi(scope, "/test") }`. Traverse for the
`JBList<SettingsListItem>` (read `key`/`title`/`description`/`badges`/`cells`) and the default-agent
`JComboBox<String>`. Teardown: `edt { ui.dispose() }`, `scope.cancel()`, `TestDialogManager.setTestDialog(TestDialog.DEFAULT)`.
Cases:
- **loads agents from cli (CLI → UI)**: mix of native (`ask`), config-overridden primary (`code`), custom
hidden, subagent, deprecated. After `flushUntil { rows present }`, assert keys/titles/merged descriptions
and badges (`custom`/`hidden`/`disabled`/`subagent`/`deprecated`), that native rows expose no `DELETE`
cell while custom rows do, and that the default-agent combo lists eligible candidates with the
`config.defaultAgent` selected.
- **changing default agent saves patch (UI → CLI)**: set the combo selection → `modified()` true →
`applyDraft()``flushUntil { appRpc.configPatches.isNotEmpty() }`; assert
`patch.values[CONFIG_DEFAULT_AGENT]` and that `modified()` returns to `false` after reload.
- **adding an agent renders it (add → CLI → UI)**: call `service<KiloAgentBehaviorService>().createAgent(...)`
(fake records the create + appends the agent) then `edt { ui.reload() }`; `flushUntil` the new agent
appears in the list with the `custom` badge and a `DELETE` cell. Assert `agentRpc.creations` captured the
input. (Documents that the modal Create action wiring isn't driven headlessly.)
- **deleting custom agent removes it (delete → CLI)**: `TestDialogManager.setTestDialog(TestDialog.YES)`;
realize the list (`edt { list.setSize(400,200); list.doLayout() }`) and dispatch synthetic
`MOUSE_PRESSED`+`MOUSE_RELEASED` (button1) at the delete cell center, computed from
`list.getCellBounds(idx,idx)` + `settingsListCellBounds(list, bounds, item, selected)` (internal helpers
used by the provider test). `flushUntil { agentRpc.removals.contains(name) }`; assert the row is gone and,
if it was the default, the picker cleared it.
- *Fallback if synthetic mouse dispatch is flaky*: keep the model-level assertion (custom rows render a
`DELETE` cell, native do not) and rely on `KiloAgentBehaviorServiceTest` for the `removeAgent` round trip.
- **reset reverts unsaved default-agent change**: change combo → `modified()``resetDraft()`
`modified()` false and combo reselected to base default.
- *(optional)* **failed config update keeps change pending**: set `appRpc.configUpdateError`, change
default agent, `applyDraft()`, flush; assert the attempt happened but `base` is unchanged.
## Files
**Add (production):**
- `packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/AgentBehaviorDto.kt` (AgentCreateDto)
- `packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloAgentBehaviorRpcApi.kt` (createAgent)
- `packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt` (createAgent)
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloAgentBehaviorService.kt` (createAgent)
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/AgentCreateState.kt` (new)
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/AgentCreateDialog.kt` (new)
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/AgentsConfigurable.kt` (wire Create action)
- `packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties` (create keys)
**Add (tests + fake):**
- `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAgentBehaviorRpcApi.kt` (createAgent override)
- `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentCreateStateTest.kt`
- `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentCreateDialogTest.kt`
- `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentEditDialogTest.kt`
- `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloAgentBehaviorServiceTest.kt`
- `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentsSettingsUiTest.kt`
No `packages/opencode/` or SDK files change (agent-builder endpoint + generated client already exist).
## Validation
Run from `packages/kilo-jetbrains/` (requires Java 21):
- `./gradlew typecheck`
- `./gradlew :frontend:test --tests "ai.kilocode.client.settings.agents.*" --tests "ai.kilocode.client.app.KiloAgentBehaviorServiceTest"`
- `./gradlew test` for a full backend+frontend run if touching the shared RPC interface.
## Notes
- `packages/kilo-jetbrains/` is entirely Kilo-owned → **no `kilocode_change` markers**; the shared
RPC/DTO + backend impl + frontend all change together (per the JetBrains AGENTS.md "files that must
change together" rule for RPC contracts).
- **No SDK/CLI regen**: the agent-builder `save` route and `DefaultApi.agentBuilderSave` already exist.
- Adding "create agent" is a **user-facing JetBrains feature** → include it in the JetBrains release
changelog at release time (per the `release-jetbrains` skill); repo changesets use non-JetBrains scopes.
- Validation/state logic is pure-tested; modal `DialogWrapper` flows (create/edit) and the
`Messages`/synthetic-click delete are exercised at the form/service/render level, never by mocking
threading.
- "Import agent" remains a placeholder (out of scope).
@@ -1,154 +0,0 @@
# JetBrains Agent Settings — Analysis Findings
Analysis of the **Agents** settings page in `packages/kilo-jetbrains/` across four dimensions:
test coverage, parity with the VS Code extension, memory/performance leaks, and conformance
to the JetBrains `AGENTS.md`. The feature is fully committed (`da064ee0``4295d11d`).
Scope of files reviewed:
- `frontend/.../settings/agents/``AgentsConfigurable.kt`, `AgentSettingsState.kt`,
`AgentCreateState.kt`, `AgentCreateDialog.kt`, `AgentEditDialog.kt`,
`AgentBehaviorConfigurable.kt`, `AgentBehaviorConfigurableBase.kt`, `Mcp/Rules/Workflows/Skills`
- `frontend/.../app/KiloAgentBehaviorService.kt`
- `frontend/.../settings/base/``SettingsListPanel.kt`, `SettingsListView.kt`,
`KiloReadyConfigurable.kt`, `SettingsRow.kt`, `SettingsToggle.kt`
- `backend/.../rpc/KiloAgentBehaviorRpcApiImpl.kt`
- `shared/.../rpc/KiloAgentBehaviorRpcApi.kt`, `shared/.../rpc/dto/AgentBehaviorDto.kt`
- VS Code parity: `webview-ui/src/components/settings/AgentBehaviourTab.tsx`,
`ModeCreateView.tsx`, `ModeEditView.tsx`
- CLI contract: `packages/opencode/src/kilocode/agent/builder.ts`
---
## 1. Test coverage
Present and solid:
| Test | Covers |
|---|---|
| `AgentSettingsStateTest` | `agentsDraft`, `updateAgent`, `patch` (changed-field/clear/native-restriction), `savedMatches`, default-agent clearing |
| `AgentCreateStateTest` | `validateAgentCreate` — blank/invalid/duplicate name, blank prompt, invalid mode, all valid modes |
| `KiloAgentBehaviorServiceTest` | `createAgent`/`removeAgent` round trips + `safe()` failure fallback, off-EDT assertions |
| `AgentsSettingsUiTest` | Real `BasePlatformTestCase` + real Swing + fake RPC: CLI→UI load, default-agent patch UI→CLI, reset, add+reload render, synthetic-click delete |
`AgentsSettingsUiTest` conforms to the AGENTS.md "do not mock the EDT" rule (real EDT, fake RPC,
`flushUntil` drains coroutines + EDT).
Gaps (significant):
- **`AgentCreateDialogTest.kt` and `AgentEditDialogTest.kt` are missing**, though the e2e-tests
plan required them (`jetbrains-agent-settings-e2e-tests.md` lines 116131, 191). The plan's
correctness argument for edit→CLI is "dialog form binding + existing state transforms"; only the
transforms half exists. Untested form-binding logic:
- `AgentCreateDialog.result()` incl. scope label→value mapping (`AgentCreateDialog.kt:61-64`),
which silently breaks if the i18n label changes.
- `AgentEditDialog.result()` — blank→null trimming, numeric parse, mode read-back, native
restriction (description non-editable, mode/visibility toggles disabled).
- `NumericFilter` decimal/integer document filter (`AgentEditDialog.kt:242-260`).
- **Modal action wiring is never exercised end-to-end.** `CreateAction.actionPerformed`
dialog → `createAgent``reload` (`AgentsConfigurable.kt:235-244`) and `onCell(EDIT_CELL)`
`AgentEditDialog.showAndGet()``updateAgent` (`AgentsConfigurable.kt:99-112`) are not driven
(headless-modal limitation). Adding the two dialog tests mostly closes this.
- Optional "failed config update keeps change pending" case not implemented.
## 2. Parity with VS Code (`AgentBehaviourTab`)
Structurally equivalent: VS Code's 5 subtabs ↔ 5 JetBrains Configurables.
At/above parity for the agents page:
- Default-agent picker with the same eligibility filter (`defaultAgentCandidate`).
- All five badges (`custom`, `subagent`, `hidden`, `disabled`, `deprecated`).
- Delete restricted to custom (non-native).
- Edit dialog covers description/prompt/model/variant/temperature/topP/steps/hidden/disable —
equal to `ModeEditView` **plus** a `mode` selector VS Code's edit lacks.
- Create dialog **adds mode + scope (project/global)** that `ModeCreateView` lacks.
Parity gaps (deferred — to be added later by the user):
- **Import agent is a no-op** (`PlaceholderAction`, `AgentsConfigurable.kt:229,267-271`); VS Code has
working `.json` import.
- **No Export agent**; VS Code's `ModeEditView` has Export.
- **Create persistence differs**: VS Code writes to config `agent` map (mode hardcoded primary);
JetBrains writes a markdown agent file via agent-builder `save` (`KiloAgentBehaviorRpcApiImpl.kt:74-86`).
- No "Browse Marketplace" on the JetBrains agents page (minor).
Validation parity confirmed: JetBrains regex `^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$` is equivalent to the
CLI's `min(1).max(64).regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/)` (`builder.ts:13-17`); prompt non-blank
matches `z.string().regex(/\S/)`.
## 3. Memory & performance / leaks
- **Panel `dispose()` never called in production for agent-behavior pages.** `AgentsSettingsUi`
extends `SettingsListPanel` (a `Disposable` that registers shortcut sets with itself as parent,
`SettingsListPanel.kt:139-141`), but `AgentBehaviorConfigurableBase.disposeReadyComponent` only
nulls the field (`AgentBehaviorConfigurableBase.kt:28-30`). Sibling configurables
(`ProvidersConfigurable:36-40`, `ModelsConfigurable`, `UserProfileConfigurable`) call
`panel.dispose()`. The coroutine is still cleaned up via scope cancellation
(`KiloReadyConfigurable.kt:74-89`), so this is a *bounded* leak (Disposer node + shortcut
registrations per page open), but an inconsistency. Applies to Agents/Workflows/Skills
(`SettingsListPanel`); Mcp/Rules are `BaseContentPanel` (not `Disposable`).
- **Blocking HTTP on the Default dispatcher.** The raw okhttp path wraps work in
`withContext(Dispatchers.IO)` (`request()`, `:132`), but typed generated-client calls do not —
`createAgent``api.agentBuilderSave(...)` (`:84`) and `agents``api.appAgents(...)` (`:47`)
run on the caller's `Dispatchers.Default` context. Violates the JetBrains threading guide.
- `EditorTextField` prompt fields are managed by the platform (released on `removeNotify`, dialog
disposes its tree) — no leak, but the missing dialog tests should baseline editor counts.
- Settings list is small-N and not a streaming surface, so the retained-component stress/leak test
requirement does not apply.
## 4. Conformance to AGENTS.md
Strong overall: RPC contract files changed together, all `@Serializable`, Kilo-owned (no
`kilocode_change` markers); `@Rpc`/`RemoteApi<Unit>`/`suspend`; RPC off-EDT; `durable {}`; EDT
discipline (`@RequiresEdt`, EDT dispatcher hops, `ActionUpdateThread.EDT`); standard Swing + IntelliJ
components, `JBUI`/`UiStyle`, theme-derived styling, `DialogWrapper` validation, complete i18n; no
UI DSL / Compose / JCEF.
Minor deviations: the dispose inconsistency and `Dispatchers.IO` omission (above); `remove()` uses
`Messages.showYesNoDialog` (`AgentsConfigurable.kt:177`) where AGENTS.md prefers non-modal (a
confirm-destroy dialog is a reasonable exception, matches VS Code).
---
## Action plan
1. **Add the two missing dialog tests** (`AgentCreateDialogTest`, `AgentEditDialogTest`) — largest gap.
2. **Honor the `Disposable` contract**`AgentBehaviorConfigurableBase.disposeReadyComponent` should
dispose the panel when it is `Disposable`.
3. **Wrap typed generated-client calls** (`createAgent`, `agents`) in `withContext(Dispatchers.IO)`.
4. *(deferred)* Implement Import (and Export) to close VS Code parity; reconcile create-persistence.
## Work completed (items 13)
- **Item 1 — dialog tests added** (both pass):
- `AgentCreateDialogTest.kt` — default empty/primary/project form, full read-back into
`AgentCreateDto`, trim + blank-description-dropped.
- `AgentEditDialogTest.kt` — loads a populated draft into the form, reads edits back
(description/prompt/temperature/topP/steps/mode/hidden/disable), and native-restriction
(description non-editable, mode/visibility toggles disabled, restricted fields unchanged).
- **Note (test seam):** `BasePlatformTestCase` is headless, so `HeadlessDialog.getRootPane()`
returns `null` and the planned `dialog.rootPane` traversal is impossible. The dialogs keep their
inputs as private fields laid out by `createCenterPanel()`. To let tests inspect the *real*
component tree (and the real `result()`), each dialog now caches its center panel and exposes it
via `internal fun contentForTest(): JComponent` — consistent with the existing
`ModelPicker.selectedForTest()`/`selectionKeyForTest()` precedent. If you prefer no test seam,
the alternative is extracting an `internal AgentCreateForm`/`AgentEditForm` component.
- **Item 2 — dispose fixed**: `AgentBehaviorConfigurableBase.disposeReadyComponent` now calls
`(panel as? Disposable)?.dispose()` (guarded; Mcp/Rules panels are `BaseContentPanel`, not
`Disposable`). `AgentsSettingsUiTest` still passes.
- **Item 3 — Dispatchers.IO**: `KiloAgentBehaviorRpcApiImpl.agents()` and `createAgent()` now wrap the
typed generated-client calls (`appAgents`, `agentBuilderSave`) in `withContext(Dispatchers.IO)`.
Validation: `./gradlew typecheck` passes; targeted frontend tests pass —
`AgentCreateDialogTest` (3), `AgentEditDialogTest` (3), `AgentsSettingsUiTest` (5),
`AgentCreateStateTest` (7), `KiloAgentBehaviorServiceTest` (4).
Item 4 (Import/Export parity, create-persistence reconciliation) deferred to the user.
## Follow-up noted while writing tests (not addressed)
- `AgentEditDialog.variant` uses a numeric-only `NumericField` (`AgentEditDialog.kt:65`), but a model
`variant` is a string (e.g. `high`). The initial value loads (set before the document filter is
installed), but the field rejects non-numeric typed edits. Likely a copy-paste from the numeric
rows; worth a separate fix.
</content>
</invoke>
@@ -1,287 +0,0 @@
# JetBrains Configurable Draft Lifecycle Refactor
## Goal
Unify the async save lifecycle for JetBrains settings pages that participate in IntelliJ Settings `isModified/apply/reset`, then fix the agent prompt stale-apply bug through that shared path.
The implementation should not be an Agents-only workaround. Models already has the right semantics in `BaseSettingsUi`; extract those semantics so Agents, Rules, Skills, and Models all share one baseline/pending/save model.
## Current Findings
IntelliJ `Configurable.apply()` is synchronous from the Settings dialog's perspective. The platform calls `apply()` and then immediately calls `isModified()`; it does not call `reset()` after apply.
Local IntelliJ source references:
- `$INTELLIJ_REPO/platform/ide-core/src/com/intellij/openapi/options/UnnamedConfigurable.java`: `apply()` stores form settings, `reset()` loads settings into the form.
- `$INTELLIJ_REPO/platform/platform-impl/src/com/intellij/openapi/options/ex/ConfigurableCardPanel.java`: calls `createComponent()` then `reset()` after component creation.
- `$INTELLIJ_REPO/platform/platform-impl/src/com/intellij/openapi/options/newEditor/SettingsEditor.java`: after `ConfigurableEditor.apply(configurable)`, the configurable is removed from the modified set only when `!configurable.isModified()`.
Affected Settings `Apply` pages:
- `ModelsSettingsUi` already uses `BaseSettingsUi`, which has `baseline`, `pending`, stale app-state rejection, failure rollback, and concurrent-edit preservation.
- `AgentsSettingsUi` duplicates a weaker version and can accept stale returned config, causing the reported prompt regression.
- `RulesSettingsUi` and `SkillsSettingsUi` both launch async saves and update their baseline only after the RPC returns, so they can also remain modified immediately after Apply and can accept stale returned config.
Not in scope for this draft lifecycle:
- `ProvidersSettingsUi` saves immediately from per-action flows, not via Settings `Apply`.
- `UserProfileConfigurable` is status/login UI with no persistent draft.
- `WorkflowsSettingsUi` is read-only.
- `McpSettingsUi` currently performs immediate remove actions, not a draft/apply page.
## Extraction Plan
1. Add a generic draft page contract under `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/`.
Suggested name: `SettingsDraftPage`.
```kotlin
internal interface SettingsDraftPage {
fun modified(): Boolean = false
fun applyDraft() = Unit
fun resetDraft() = Unit
}
```
2. Move the generic ready-configurable delegation into a superclass.
Suggested name: `DraftReadyConfigurable<T : JComponent>`.
Responsibilities:
- Store the created ready UI component.
- Delegate `isModifiedReady()`, `applyReady()`, and `resetReady()` to `SettingsDraftPage` when the component implements it.
- Dispose the retained component if it implements `Disposable`.
- Leave project-directory lookup to subclasses.
3. Refactor `AgentBehaviorConfigurableBase` to extend `DraftReadyConfigurable<T>`.
Keep `AgentBehaviorConfigurableBase` only for agent-behavior-specific project directory resolution and `create(cs, dir)`.
4. Refactor `ModelsConfigurable` to extend `DraftReadyConfigurable<ModelsSettingsUi>`.
This removes the duplicate `ui?.modified()`, `ui?.applyDraft()`, `ui?.resetDraft()`, and disposal wiring while preserving model-specific directory resolution.
5. Replace `AgentBehaviorPage` with `SettingsDraftPage`.
Agents, Rules, and Skills should implement the base contract directly. Remove the agent-specific interface after callers are migrated.
6. Extract `BaseSettingsUi` draft-save semantics into a shared state helper.
Suggested name: `SettingsDraftState<D>`.
Responsibilities:
- Track `base`, `draft`, `pending`, `saving`, and `error`.
- Compute `modified()` as `draft` versus `pending ?: base` using a supplied saved-equality predicate.
- Start a save synchronously by setting `pending = draft`, `saving = true`, and clearing errors before the async RPC is launched.
- Accept external/app-state base updates only when there is no pending save, or when the update matches the pending target.
- Complete a successful save by accepting returned base only if it matches the applied target; otherwise use the applied target as the new base.
- Complete a failed save by restoring the previous base, keeping the edited draft visible, clearing pending, and leaving `modified()` true.
- Preserve user edits made while a save is in flight.
7. Refactor `BaseSettingsUi` to use `SettingsDraftState<D>` internally.
Keep its public/protected API stable where possible:
- `protected var draft`
- `protected val saving`
- `protected val saveError`
- `modified()`
- `resetDraft()`
- `applyDraft()`
- `acceptBase(base)`
The goal is for existing `ModelsSettingsUi` behavior and tests to continue passing while the reusable lifecycle moves out of `BaseSettingsUi`.
8. Use `SettingsDraftState` in `AgentsSettingsUi`.
Specific agent behavior:
- Initialize the state with `agentsDraft(app.state.value.config, emptyList())` and `savedMatches`.
- In `fetch()`, build `next = agentsDraft(currentConfig, agents)` and pass it through the shared base-accept path.
- Preserve the existing agent-details merge behavior for local dirty drafts, so newly discovered or removed agents do not disappear while the user is editing.
- Do not treat `modified() == false` during a pending save as permission to overwrite the draft from stale app config.
- In `applyDraft()`, use the shared start/complete/fail flow with `patch(base, draft)` and `KiloAppService.updateConfig(change)`.
- On successful but stale returned config, keep the applied draft as the new base so reopening `ask` shows the saved prompt without a manual refresh.
9. Use `SettingsDraftState` in `RulesSettingsUi`.
Specific rules behavior:
- Use `List<String>` or a tiny `RulesDraft` as the draft type.
- The `SettingsListEditor` callback updates `state.draft` through the shared update path.
- `applyDraft()` patches `ConfigPatchDto(instructions = draft)` through the shared lifecycle.
- `resetDraft()` resets through the shared lifecycle and updates the editor.
- Successful but stale returned config keeps the applied rules as the base.
- Failed saves keep the edited rules visible and modified.
10. Use `SettingsDraftState` in `SkillsSettingsUi`.
Specific skills behavior:
- Introduce `SkillsDraft(paths: List<String>, urls: List<String>)` if that keeps state readable.
- `modified()`, `resetDraft()`, and `applyDraft()` should all use the shared lifecycle.
- `fetch()` accepts current app config into the shared base state but must preserve dirty or pending local `paths/urls` edits.
- Local remove actions update the draft state, then refresh the rendered rows.
- Successful but stale returned config keeps the applied skills draft as the base.
- Failed saves keep the edited skills visible and modified.
## Backend Hardening
Keep the backend fix from the original plan because it protects every frontend settings caller.
Update `packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt` so `updateConfig(...)` never returns the pre-patch `Ready` state after a successful `PATCH /global/config`.
Preferred approach:
- Keep sending the raw `PATCH /global/config` request with `KiloCliDataParser.buildConfigPatch(...)`.
- After a successful PATCH, fetch fresh config with `fetchConfig()` and warnings with `fetchWarnings()`.
- Build and return a `KiloAppState.Ready(current.data.copy(config = cfg, warnings = warns))` from the fresh config.
- Update `_appState` only when race checks show it is still safe, following the existing `refreshConfigState()` pattern.
- Throw if fresh config cannot be fetched after a successful PATCH instead of returning stale `current`; the frontend shared failure path will keep the draft modified.
## Tests
1. Add pure tests for `SettingsDraftState`.
Cover:
- Baseline edit and reset.
- Pending save target is not modified immediately after apply starts.
- New edits during a pending save become modified.
- Matching external base update accepts pending target.
- Stale external base update is ignored while pending.
- Successful save with matching returned base accepts returned base.
- Successful save with stale returned base falls back to applied target.
- Failed save keeps draft dirty and restores previous base.
- Concurrent edit is preserved after save completion.
2. Keep `BaseSettingsUiTest` and `BaseSettingsUiWorkspaceTest`, but adjust them to assert integration with the extracted state rather than owning all lifecycle behavior.
The existing tests already cover pending save, failed save, concurrent edit preservation, login banner stability, workspace load, and app/model state delivery.
3. Keep `ModelsSettingsUiTest` passing after the refactor.
These tests are important regression coverage because Models is the known-good implementation. They should verify that the extracted lifecycle preserves existing behavior.
4. Add `AgentsSettingsUiTest` coverage for the exact reported bug.
Use the real `AgentsSettingsUi`, real frontend services, fake RPC APIs, and real Swing component tree.
Use `com.intellij.ui.UiInterceptors.register(...)` to intercept the next `DialogWrapper` opened by clicking the `ask` row edit cell. Cast to `AgentEditDialog`, mutate the prompt field in `contentForTest()`, and call `performOKAction()`.
Assert:
- The prompt edit makes the page modified.
- `applyDraft()` makes `modified()` false immediately when no further edits were made.
- The recorded config patch contains `agents["ask"].prompt == "new"`.
- Reopening `ask` shows the new prompt, not the old prompt, without manual refresh.
5. Add a stale-return `AgentsSettingsUiTest`.
Extend `FakeAppRpcApi` with a test mode such as `configUpdateReturnStale = true` or a `configUpdateResult` callback.
The fake should still record and apply the patch to its internal state, but return the pre-patch state from `updateConfig(...)`.
Assert that the UI still reopens the agent with the applied prompt and `modified()` is false after save completion.
6. Add pending/failure `AgentsSettingsUiTest` coverage.
Use `FakeAppRpcApi.configUpdateGate` to block save completion.
Assert:
- `modified()` is false while the pending save target matches the draft.
- A second edit during the pending save makes `modified()` true.
- Releasing the gate preserves the second edit instead of overwriting it.
- `configUpdateError` leaves the edited prompt visible and `modified()` true.
7. Add `RulesSettingsUiTest` coverage.
Use real UI components where practical by finding the `SettingsListEditor` text field and Add button in the component tree.
Assert:
- Adding/removing a rule makes the page modified.
- `applyDraft()` makes `modified()` false immediately.
- Successful stale returned config keeps the applied rules as base.
- Failed save keeps edited rules visible and modified.
- Reset during pending save returns to the pending target.
8. Add `SkillsSettingsUiTest` coverage.
Seed `KiloAppService` config with `skills.paths` and `skills.urls`, and use `FakeAgentBehaviorRpcApi` for discovered skills.
Assert:
- Removing a local path or URL makes the page modified.
- `applyDraft()` makes `modified()` false immediately.
- Successful stale returned config keeps the applied skills as base.
- Failed save keeps edited skills visible and modified.
- Reload/fetch during a pending save does not overwrite the pending target from stale app config.
9. Add backend regression coverage in `KiloBackendAppServiceTest`.
Scenario:
- Seed `mock.config` with `ask.prompt = "old"` and connect until `Ready`.
- Change `mock.config` to `ask.prompt = "new"` before or during `svc.updateConfig(...)` so the next config fetch represents post-patch server state.
- Push `global.disposed` around the update to exercise the reload race.
- Call `svc.updateConfig(ConfigPatchDto(agents = mapOf("ask" to AgentConfigPatchDto(prompt = "new"))))`.
- Assert the returned `KiloAppState.Ready` contains `ask.prompt == "new"` and not the old prompt.
- Assert `mock.lastConfigPatchBody` contains the prompt patch.
10. Do not add production-only test accessors.
Use existing real UI traversal patterns, `UiInterceptors`, fake RPC APIs, EDT execution through `BasePlatformTestCase`, and `UIUtil.dispatchAllInvocationEvents()`.
## Files
Production files likely touched:
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsDraftPage.kt` or equivalent new base file.
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsDraftState.kt` or equivalent new base file.
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/BaseSettingsUi.kt`.
- `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/agents/AgentBehaviorConfigurableBase.kt`.
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/AgentsConfigurable.kt`.
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/RulesConfigurable.kt`.
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt`.
- `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAppRpcApi.kt`.
- `packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt`.
Test files likely touched or added:
- `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsDraftStateTest.kt`.
- `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/BaseSettingsUiTest.kt`.
- `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/BaseSettingsUiWorkspaceTest.kt`.
- `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUiTest.kt`.
- `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentsSettingsUiTest.kt`.
- `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/RulesSettingsUiTest.kt`.
- `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt`.
- `packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendAppServiceTest.kt`.
## Validation
Run from `packages/kilo-jetbrains/`:
```sh
./gradlew :frontend:test --tests "ai.kilocode.client.settings.base.*" --tests "ai.kilocode.client.settings.models.ModelsSettingsUiTest" --tests "ai.kilocode.client.settings.agents.*SettingsUiTest"
./gradlew :backend:test --tests "ai.kilocode.backend.app.KiloBackendAppServiceTest"
./gradlew typecheck
```
Run the full package test suite if shared settings base changes have wider impact:
```sh
./gradlew test
```
## Notes
All planned source changes are under `packages/kilo-jetbrains/`, which is Kilo-owned, so no `kilocode_change` markers are needed.
No CLI or SDK regeneration should be needed.
The user-visible outcome is that every Settings `Apply` draft page has consistent async-save behavior, and agent prompt edits reopen with the newly saved value without requiring manual refresh.
@@ -1,65 +0,0 @@
# JetBrains Agents Layout Adjustments
## Goal
Update the JetBrains Agents settings list layout to match the requested visual behavior without wiring edit/import/create/delete handlers yet.
## Scope
- Package: `packages/kilo-jetbrains/`.
- UI only, plus focused tests.
- Do not change VS Code sources.
- Preserve Swing-only implementation, theme-derived colors, `JBUI`/`UiStyle` spacing, and EDT requirements from `packages/kilo-jetbrains/AGENTS.md`.
## Implementation Plan
1. Adjust neutral badge styling to match VS Code's subtle badge intent.
- Update `UiStyle.Colors.badgeBg()` so neutral badges do not use the platform blue `Badge.background` when available.
- Use a subtle theme-derived background comparable to VS Code's `bg-subtle-base` fallback behavior.
- Keep neutral badge text on `UiStyle.Colors.weak()` or equivalent theme-derived weak foreground.
- Keep warning badges for deprecated agents unchanged.
2. Lower-case badge labels.
- Update `KiloBundle.properties` badge strings to lower-case: `custom`, `hidden`, `deprecated`, `subagent`, and any currently defined badge labels such as `built-in` and provider `env`.
- Keep all user-visible labels localized through bundle keys.
3. Remove the Agents toolbar remove action.
- Delete the `AgentsSettingsUi.trailingActions()` override.
- Remove the Agents toolbar `Remove` action helper and its selected-row enablement logic.
- Keep the shared `SettingsListPanel.trailingActions()` hook only if another current or near-term consumer still needs it; otherwise prune it to avoid an unused abstraction.
4. Update the `+` popup labels.
- Keep the toolbar `+` as a popup action group with no handlers.
- Change popup child labels to `New Agent...` and `Import Agent...`.
- Prefer the order requested by the user: `New Agent...`, then `Import Agent...`.
- Keep action bodies as no-ops.
5. Add list-element buttons for Agents using the provider-style cell mechanism.
- Reuse `SettingsListItem.cells` so buttons appear on the selected row like provider list actions.
- Add an `edit` text cell for each agent row.
- Add a delete icon cell only for custom/removable agents.
- Do not implement any edit/delete behavior yet; `onCell` should ignore these cells or no-op.
- Remove the old row-level removal behavior from Agents if any remains.
6. Extend shared list cells to support icon-only cells.
- Add optional icon support to `SettingsListCell` with defaults that leave provider text cells unchanged.
- Render icon-only cells as the same styled action cell chrome used by provider actions.
- Use `AllIcons.Actions.GC`, matching existing JetBrains history delete UI.
- Keep the text label as accessibility/tooltip text for the icon-only delete cell.
- Update hit testing width calculation to account for icon-only and icon-plus-text cells.
7. Update focused tests.
- Update `AgentsSettingsUiTest` toolbar expectations to `Add agent`/`Refresh` plus the right-side default picker, with no toolbar `Remove`.
- Update popup expectations to `New Agent...` and `Import Agent...`, and assert no side effects.
- Replace the toolbar remove behavior test with selected-row cell layout assertions: native row has `edit`, custom row has `edit` plus delete icon.
- Assert clicking/triggering edit/delete cells does not call removal or config patch APIs yet.
- Update badge assertions to lower-case strings and confirm primary remains omitted.
- Keep provider tests passing to protect shared cell rendering changes.
## Validation
Run from `packages/kilo-jetbrains/`:
- `./gradlew :frontend:test --tests ai.kilocode.client.settings.agentbehavior.AgentsSettingsUiTest`
- `./gradlew :frontend:test --tests ai.kilocode.client.settings.providers.ProvidersSettingsUiTest`
- `./gradlew typecheck`
- `./gradlew test`
## Notes
- This is layout-only for Agents edit/delete/import/create. No dialogs, config patches, RPC calls, or deletion confirmation should be added in this pass.
- Shared renderer changes must be backward-compatible for Providers, Rules, Skills, and Workflows.
@@ -1,162 +0,0 @@
# JetBrains Agents settings toolbar and badges
Update the JetBrains **Agent Behavior → Agents** settings list so it matches the requested toolbar layout and VS Code badge behavior while keeping the implementation in the shared settings-list architecture.
All implementation paths are under `packages/kilo-jetbrains/`. This is Kilo-owned code, so no `kilocode_change` markers are needed.
## Goals
- Put the Agents toolbar on one horizontal row:
- left: `+ | Refresh | -`
- right: `Default Agent: <picker>`
- Make the right-side toolbar content a reusable optional feature of `SettingsListPanel`, not a one-off in `AgentsConfigurable`.
- Make `+` a popup action group with placeholder actions for **Import Agent** and **Create New Agent**, matching VS Code concepts but without implementing handlers in this pass.
- Move agent-mode/config string constants out of frontend UI code into shared CLI parsing/constants code.
- Match VS Code-style agent badge behavior:
- do not render a `primary` badge
- render `subagent` only for subagents
- use the same subtle/warning badge color intent as VS Code
- Stop using the new `BadgeLabel` wrapper in the shared renderer; use `FilledBadgeIcon` directly.
## Current Findings
- `AgentsConfigurable.kt` currently returns a `headerExtras()` `SettingsRow` for the default-agent picker, so it appears below the toolbar instead of right-aligned in the toolbar.
- `SettingsListPanel.kt` currently builds a single left toolbar from `extraActions()` plus optional refresh; it has no right-side content hook and no way to put actions after Refresh.
- `AgentsConfigurable.kt` currently hardcodes `"subagent"` when filtering default-agent options and currently renders `SettingsBadge(item.mode)`, which displays `primary`.
- `KiloCliParser` already exists in `shared/src/main/kotlin/ai/kilocode/cli/KiloCliParser.kt`, so shared CLI constants can live there without adding a frontend-only constant holder.
- VS Code agent settings render `custom`, `subagent`, and `hidden` with subtle badge colors and `deprecated` with warning colors. They do not display a primary badge.
- `FilledBadgeIcon` already provides the pill renderer. The newly added `BadgeLabel` is just a wrapper around it.
- JetBrains currently has no agent create/import implementation. The requested `+` menu should therefore be UI-only placeholders for now.
## Implementation Plan
### 1. Extend `SettingsListPanel` toolbar composition
In `frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListPanel.kt`:
- Keep the existing header structure with toolbar first and search below.
- Add optional hooks for toolbar layout:
- left actions before Refresh, defaulting to existing `extraActions()` behavior
- actions after Refresh, defaulting to empty
- right toolbar content, defaulting to `null`
- Build the toolbar row with a `BorderLayout` or equivalent Swing layout:
- WEST: IntelliJ `ActionToolbar` for actions
- EAST: optional right content
- Insert separators only when adjacent action groups exist, so Agents can render `+ | Refresh | -` while Rules can still render `Add | Refresh` and other pages remain unchanged.
- Preserve existing keyboard behavior for search Enter/Up/Down and existing refresh shortcut registration.
- Keep all Swing-touching methods annotated/guarded with `@RequiresEdt` and `checkEdt()`.
### 2. Expose selected row state for toolbar actions
In `SettingsListView.kt`:
- Add an EDT-only way to read the selected `SettingsListItem` or selected key.
- Add an optional selection listener or callback so a parent panel can request toolbar updates when selection changes.
- Keep selection preservation by key when lists reload or filter.
This enables an Agents `-` toolbar action that is disabled unless the selected row is removable.
### 3. Move Agents default picker into the toolbar right side
In `AgentsConfigurable.kt`:
- Replace `headerExtras()` with the new right-toolbar hook.
- Render a compact horizontal control: `Default Agent:` label plus `JComboBox<String>`.
- Keep existing draft/base behavior:
- `modified()` remains `draft != base`
- `applyDraft()` still writes `default_agent`
- `resetDraft()` restores the picker
- `afterApply()` repopulates picker options after reload
- Use shared CLI constants for the `default_agent` config key and agent-mode comparisons.
### 4. Add Agents left toolbar actions
In `AgentsConfigurable.kt`:
- Add a `+` popup `ActionGroup` with two child actions:
- `Import Agent`
- `Create New Agent`
- Do not implement import/create behavior in this pass. The child actions should be explicit placeholders with no config mutation, RPC call, file picker, or dialog flow.
- Add `Refresh` through the shared panel refresh hook.
- Add a `-` action after Refresh:
- enabled only when the selected row is a custom/removable agent and the panel is not busy
- invokes existing `KiloAgentBehaviorService.removeAgent(dir, key)` and reloads
- Remove Agents row-level `Remove` cells to avoid duplicate deletion affordances. Other list pages keep their selected-row remove cells.
- Add i18n keys for the add group and placeholder child actions.
### 5. Centralize CLI constants
In `shared/src/main/kotlin/ai/kilocode/cli/KiloCliParser.kt`:
- Add shared constants for agent modes:
- `primary`
- `subagent`
- `all`
- Add a shared constant for the `default_agent` config key used by Agents settings.
- Add small helper predicates if they keep UI code free of literals, for example:
- `isSubagent(mode)`
- `defaultAgentCandidate(mode, hidden)`
- Update touched frontend code to use these constants/helpers instead of hardcoded mode/config strings.
- Do not move user-visible badge labels into these constants; those stay in `KiloBundle.properties`.
### 6. Update badge rendering and colors
In `SettingsListRenderer.kt` and `UiStyle.kt`:
- Replace `BadgeLabel` usage with direct `JBLabel` instances whose `icon` is a `FilledBadgeIcon`.
- Delete `BadgeLabel.kt` if no longer used.
- Adjust badge tone mapping to match VS Code intent:
- neutral/subtle badges use theme-derived subtle badge background and weak/badge foreground
- warning badges use theme-derived warning color for the badge surface and a readable contrast foreground
- avoid raw color literals in runtime UI code
- Keep the renderer test accessors by reading `FilledBadgeIcon.text` from label icons.
In `AgentsConfigurable.kt`:
- Replace `SettingsBadge(item.mode)` with mode-specific logic:
- add localized `Subagent` badge only when the agent mode is shared `subagent`
- never add `Primary` for `primary`
- do not add a badge for `all` unless explicitly requested later
- Use neutral/subtle tone for `Custom`, `Hidden`, and `Subagent` to match VS Code.
- Use warning tone for `Deprecated`.
- Add `settings.agentBehavior.badge.subagent=Subagent` to `KiloBundle.properties`.
### 7. Tests
Add focused tests under `frontend/src/test/kotlin/ai/kilocode/client/settings/agentbehavior/`:
- Add `FakeAgentBehaviorRpcApi` in `client/testing` if needed, mirroring the existing provider/app fake pattern.
- Add `AgentsSettingsUiTest` covering:
- toolbar has add group, refresh, and remove on the left
- default-agent label/picker is in right toolbar content, not below the toolbar
- add group exposes `Import Agent` and `Create New Agent` placeholder actions
- placeholder add/import actions do not mutate config or call agent RPC
- remove toolbar action is disabled for native agents and enabled for custom selected agents
- remove action calls `removeAgent` and reloads
- primary agents do not render a `primary` badge
- subagent agents render only the localized `Subagent` mode badge
- custom/hidden/deprecated badges remain present with expected labels
- Update any renderer test helpers affected by removing `BadgeLabel`.
### 8. Verification
Run from `packages/kilo-jetbrains/`:
- `./gradlew :frontend:test --tests ai.kilocode.client.settings.agentbehavior.AgentsSettingsUiTest`
- `./gradlew :frontend:test --tests ai.kilocode.client.settings.providers.ProvidersSettingsUiTest`
- `./gradlew typecheck`
- `./gradlew test`
## Non-Goals
- Do not implement real agent import behavior.
- Do not implement real custom agent creation/editing behavior.
- Do not change MCP settings.
- Do not change Providers behavior beyond whatever shared renderer updates require.
- Do not modify unrelated dirty worktree files except where the implementation directly touches them.
## Risks
- Toolbar action updates may not refresh automatically after list selection changes; mitigate by wiring a selection callback and explicitly updating the toolbar presentation.
- Badge color changes are shared renderer behavior, so provider/skills/workflow badge snapshots or tests may need small expectation updates.
- Empty add/import actions can look functional even though they are placeholders; mitigate with clear i18n labels and no side effects, matching the requested scope.
@@ -1,72 +0,0 @@
# JetBrains List Badge And Tooltip Polish
## Goal
Apply the requested visual polish to the JetBrains settings list implementation without adding any new action behavior.
## Scope
- Package: `packages/kilo-jetbrains/`.
- Affects shared list primitives plus Providers and Agents settings rows.
- No action implementation for Agents edit/delete/import/create.
- Follow `packages/kilo-jetbrains/AGENTS.md`: Swing only, theme-derived colors/icons, EDT-safe UI code, no Kotlin UI DSL.
## Implementation Plan
1. Remove provider `custom` badges.
- Update `ProviderListRow.badges` so `provider.source == "custom"` no longer emits a badge.
- Keep any existing non-custom provider badges that are intentional, such as `env`.
- Add or update provider regression coverage to assert custom providers have no badges.
2. Keep/restore Agents `subagent` badge.
- Confirm `AgentsSettingsUi.fetch()` emits `settings.agentBehavior.badge.subagent` for rows where `KiloCliParser.isSubagent(item.mode)` is true.
- Keep the label lower-case `subagent` per the prior request.
- Keep primary agents badge-free.
- Update the focused Agents test so this is explicitly protected.
3. Capitalize the Agents row edit action.
- Change `settings.agentBehavior.edit` from `edit` to `Edit`.
- Keep the internal cell id as `edit`.
- Update focused Agents tests to expect visible text `Edit`.
4. Make the Agents delete icon bare.
- Keep the delete cell icon-only and layout-only.
- Use the platform delete icon (`AllIcons.Actions.GC`) so light/dark variants follow the current IDE theme.
- Do not draw the action-label background or border for icon-only delete cells.
- Prefer a minimal shared cell flag such as `chromed: Boolean = true` or a renderer branch for `iconOnly` cells; leave provider text action cells unchanged.
- Keep tooltip/accessibility text for the icon-only delete cell as `delete`.
- Update Agents tests to assert the delete cell has the icon but no visible text and uses bare styling/no action-label background.
5. Disable IntelliJ expanded-row tooltip behavior for all shared settings lists.
- In `SettingsListView`, call `list.setExpandableItemsEnabled(false)` on the shared `JBList`.
- This removes the row-expanded hover rendering from all list-based settings pages using `SettingsListView`.
6. Add a regular formatted tooltip for list row descriptions.
- Implement row tooltip behavior on the shared settings list only.
- Override `getToolTipText(MouseEvent)` for the `JBList` in `SettingsListView`.
- Resolve the hovered index with `locationToIndex`, verify the point is inside that row's bounds, and return `null` when no row or no description is present.
- Format `SettingsListItem.description` as safe HTML so longer descriptions wrap/read cleanly rather than showing a raw one-line tooltip.
- Escape description text before inserting it into HTML; preserve line breaks using `<br>` or an IntelliJ HTML helper.
- Do not set tooltips on renderer row components; the list should own the regular tooltip.
7. Update tests.
- Agents focused tests:
- `Edit` visible text.
- delete icon remains icon-only and bare.
- `subagent` badge appears for subagents.
- Provider focused tests:
- custom provider rows have no `custom` badge.
- Shared list tooltip test:
- expandable row tooltips are disabled on the shared `JBList` if accessible through the public API.
- hovering a row with a description returns formatted escaped HTML.
- hovering a row without description or outside a row returns `null`.
## Validation
Run from `packages/kilo-jetbrains/`:
- `./gradlew :frontend:test --tests ai.kilocode.client.settings.agentbehavior.AgentsSettingsUiTest`
- `./gradlew :frontend:test --tests ai.kilocode.client.settings.providers.ProvidersSettingsUiTest`
- If shared tooltip behavior gets its own test class, run that focused test too.
- `./gradlew typecheck`
- `./gradlew test`
## Notes
- Do not edit VS Code files for this pass.
- Keep the provider list action cells visually unchanged.
- Keep Agents edit/delete cells layout-only; no dialogs, config patches, RPC calls, or deletion confirmation.
@@ -1,72 +0,0 @@
# JetBrains Settings List Action Hit Targets
Fix inconsistent clicks on inline settings-list actions such as **Edit** in Agents and **Connect/OAuth/Disconnect** in Providers by making rendering and hit testing share one action-cell implementation.
## Current Findings
- Production Agents use `SettingsListView` + `SettingsListRenderer` from `settings/base/`.
- Production Providers also now use `SettingsListView` through `ProvidersContent`.
- `ProviderListRenderer` is no longer used by production code, but provider tests still depend on its provider-specific wrappers around shared hit-testing.
- The visible action label is rendered by private `SettingsListRenderer.CellLabel`, while click geometry is estimated separately in `SettingsListModel.settingsListCellWidth/settingsListCellHeight`.
- This split can make the sensitive area differ from the painted button area.
- `SettingsListView` already centralizes click dispatch through `settingsListCellAt`, so the fix should stay there and in shared base helpers, not in agents/providers.
## Implementation Plan
1. Add a shared action-cell component in `settings/base/`, for example `SettingsListActionCell`.
- Use it from `SettingsListRenderer.syncCells(...)` instead of the private `CellLabel`.
- Keep current visual behavior: text actions get `UiStyle.Components.actionLabel(...)`; icon-only actions keep their current icon-only appearance.
- Provide a small shared sizing helper, for example `settingsListCellSize(list, cell)`, that builds/configures the same component with the list font and returns its preferred size.
2. Update shared hit testing in `SettingsListModel.kt`.
- Make `settingsListCellBounds(...)` use the shared sizing helper rather than duplicating font/border math.
- Keep right alignment and gap behavior the same.
- Keep disabled-cell filtering in `settingsListCellAt(...)` so disabled env-provider disconnect remains visible but not actionable.
- For icon-only actions, preserve or slightly expand the clickable target so delete remains easy to click even if the icon has no visible button border.
3. Make action-click dispatch fully shared and button-like in `SettingsListView.kt`.
- Track the action cell resolved on `mousePressed` using `settingsListCellAt(...)`.
- On `mouseReleased`, invoke only when the same row/cell is still under the pointer and enabled.
- This makes the whole shared action-cell rectangle clickable and avoids inconsistent press/release behavior.
- Keep double-click-to-primary-row behavior, but ignore double-clicks that start on an action cell.
- Preserve the existing uncommitted double-click change already in this file.
4. Remove provider-specific action hit-test/render wrappers.
- Delete `settings/providers/ProviderListRenderer.kt` if production remains free of references.
- Keep provider domain mapping in `ProviderListRows.kt` (`ProviderListAction`, labels, enabled rules, `alwaysVisible`).
- Keep `ProvidersContent.activate(...)` as the only provider-specific action dispatcher.
5. Update tests to target shared behavior.
- Move action-bounds/action-at assertions from `ProvidersSettingsUiTest` to generic `SettingsListViewTest` or shared helper tests.
- Add coverage that clicking near the edges of the rendered action bounds invokes the action.
- Add coverage that disabled cells do not invoke actions.
- Add coverage that always-visible provider disconnect remains hit-testable while unselected.
- Update provider renderer tests to instantiate `SettingsListRenderer` directly where they verify labels, icon visibility, descriptions, foregrounds, and painting.
- Remove remaining `ProviderListRenderer` imports/usages.
6. Add a patch changeset for the user-facing JetBrains settings bug fix.
- Suggested file: `.changeset/jetbrains-settings-action-clicks.md`.
- Suggested text: `Make JetBrains settings list actions easier and more reliable to click.`
## Files To Change
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListRenderer.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListModel.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListView.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/providers/ProviderListRenderer.kt` (delete if unused)
- `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt`
- `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/providers/ProvidersSettingsUiTest.kt`
- `.changeset/jetbrains-settings-action-clicks.md`
## Validation
Run from `packages/kilo-jetbrains/`:
- `./gradlew :frontend:test --tests ai.kilocode.client.settings.base.SettingsListViewTest --tests ai.kilocode.client.settings.providers.ProvidersSettingsUiTest`
- `./gradlew typecheck`
## Notes
- Do not touch unrelated untracked `.kilo/plans/*` files.
- Preserve the current uncommitted agent edit dialog polish and existing `SettingsListView` double-click behavior while modifying shared click handling.
- All touched source paths are Kilo-owned JetBrains files; no `kilocode_change` markers are needed.
@@ -1,372 +0,0 @@
# JetBrains settings — unified list architecture (agents / rules / skills / workflows + providers)
Make the **Agents**, **Rules**, **Skills**, and **Workflows** settings pages render as real
`JBList`-based lists like the **Providers** page, with a filter field, a refresh toolbar, inline
remove buttons that appear only on the selected row, and theme-aware badge pills. Extract the
shared list machinery into common classes and **adopt them in Providers too** so there is one
list implementation, not two.
This is an architecture-first pass: **no new create/edit** of agents/rules/skills/workflows.
Existing config controls are handled per the decisions below.
All paths are under `packages/kilo-jetbrains/`. Everything here is Kilo-owned (no `kilocode_change`
markers needed).
---
## Decisions (from clarification)
1. **Edit controls:** keep simple config controls (Agents *default-agent* combo, Rules *Claude Code
compat* toggle) in a header area; **drop the string add-editors** for skill paths / skill URLs
(those become remove-only lists). **Rules instructions keep an Add affordance** (toolbar `+`).
2. **Rules page:** convert config `instructions` into the unified list with remove-on-select, and
**retain Add** (no discovered "rules" exist in the CLI — rules are purely `config.instructions`).
3. **Providers:** extract shared classes and **refactor Providers onto them too** (max dedup).
Provider-specific behavior (OAuth, dialogs, custom-provider, section bucketing, connected-row
action rules) must be preserved; provider tests are updated to the shared APIs.
Deliberate interim UX: skill **paths/urls** become remove-only (you can remove, not add) until the
edit story is rebuilt. This is intentional per decision 1.
---
## Current state (reference)
- **Providers** (`settings/providers/`) already uses the target pattern:
`JBList` + `CollectionListModel<ProviderListRow>` + custom `ProviderListRenderer` (panel renderer
with `SimpleColoredComponent` title, inline action labels, hit-testing) + `ActionToolbar`
(Add/Refresh `DumbAwareAction`) + `SearchTextField` filter + request-guarded async load with a
loading/error overlay (`SettingsPanel`/`SettingsOverlayPanel`). Inline actions are shown only when
the row is selected, except a connected row keeps **Disconnect** visible (`visibleActions`).
- **Agents/Rules/Skills/Workflows** (`settings/agentbehavior/`) use `BaseContentPanel` + `SettingsRow`
+ always-visible `JButton` removes, no filter, no toolbar, no badges.
- **Badges**: `FilledBadgeIcon` (`client/ui/FilledBadgeIcon.kt`) draws a rounded pill; the model
picker and history list each wrap it in a private `BadgeLabel`. Colors come from `UiStyle.Colors`
badge tokens.
- **Data** (`KiloAgentBehaviorService``KiloAgentBehaviorRpcApi`):
- Agents `AgentDetailDto`: `name, displayName, description, mode (subagent|primary|all), native,
hidden, deprecated`. Custom = `native != true`; remove only for custom (`removeAgent`).
- Skills `SkillDto`: `name, description, location`. Built-in = `location == "builtin"`; remove only
for custom (`removeSkill`).
- Workflows `CommandDto`: `name, description, source (command|mcp|skill), template`. Read-only.
- Rules: `config.instructions: List<String>` + `claudeCodeCompat()` toggle. No discovered list.
- **MCP** (`McpConfigurable`) is a sibling list but **out of scope** here (not requested).
---
## Shared classes to extract
New common code in `frontend/.../settings/base/` (UI tokens in `client/ui/`).
### 1. Render model — `settings/base/SettingsListModel.kt`
```kotlin
enum class SettingsBadgeTone { NEUTRAL, ACCENT, WARNING }
data class SettingsBadge(val text: String, val tone: SettingsBadgeTone = SettingsBadgeTone.NEUTRAL)
// one inline action button drawn in the renderer (e.g. "Remove", "Connect")
data class SettingsListCell(
val id: String,
val label: String,
val enabled: Boolean = true,
val alwaysVisible: Boolean = false, // visible even when the row is not selected
)
interface SettingsListItem {
val key: String
val title: String
val description: String? get() = null
val icon: Icon? get() = null
val section: String? get() = null
val badges: List<SettingsBadge> get() = emptyList()
val cells: List<SettingsListCell> get() = emptyList()
val disabled: Boolean get() = false
}
```
Top-level helpers generalized from `ProviderListRenderer` companion + `ProviderListRows`:
- `settingsListSectionTitle(items, index): String?` — section caption when the section changes.
- `settingsListVisibleCells(item, selected): List<SettingsListCell>` — `disabled → empty`, else
`cells.filter { selected || it.alwaysVisible }`. This single rule covers both the generic
"remove only when selected" requirement and the provider "connected keeps Disconnect"
(`alwaysVisible = true` on that cell).
- `settingsListCellBounds(list, bounds, item, selected): Map<String, Rectangle>` and
`settingsListCellAt(list, bounds, point, item, selected): String?` — geometry + hit-testing
(returns the cell id only when `cell.enabled`), ported from `ProviderListRenderer`.
### 2. `client/ui/BadgeLabel.kt` (shared)
Promote the model picker's private `BadgeLabel` to a shared class next to `FilledBadgeIcon`:
```kotlin
internal class BadgeLabel : JBLabel() {
init { border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap()) }
fun set(text: String?, bg: Color, fg: Color) {
isVisible = text != null
icon = text?.let { FilledBadgeIcon(it, bg, fg) }
}
}
```
`ModelPickerRenderer` and `HistoryListRenderer` may later drop their private copies in favor of this
(optional cleanup; not required for the feature).
### 3. `settings/base/SettingsListRenderer.kt`
`JPanel(BorderLayout), ListCellRenderer<SettingsListItem>` — the generic version of
`ProviderListRenderer`:
- `GroupHeaderSeparator` top (caption from `settingsListSectionTitle`, hide line at index 0).
- Left icon (`JBLabel`, hidden when `icon == null`).
- Title `SimpleColoredComponent` (bold, `UIUtil.getListForeground(selected, focus)`).
- Badges: a horizontal `Stack` of `BadgeLabel`s rebuilt per row from `item.badges` (tone → colors:
`NEUTRAL → Colors.badgeBg/badgeFg`, `ACCENT → Colors.activityBadgeBg/activityBadgeFg`,
`WARNING → Colors.warningLabelForeground`-derived). Title + badges share a head row; description
sits below.
- Description `JBLabel` (weak, hidden when blank).
- Cells: right-aligned horizontal `Stack` of action labels via `settingsListVisibleCells`, styled
with `UiStyle.Components.actionLabel`.
- `PickerRow` selection wrapper + `UiStyle.Components.transparent(...)` (same as providers/model
picker).
- Test accessors: `cellTexts()`, `badgeTexts()`, `descriptionText()`, `iconVisible()`, `iconSize()`.
### 4. `settings/base/SettingsListView.kt`
`BaseContentPanel` holding the list (generalized `ProvidersContent`):
- `JBList<SettingsListItem>` + `CollectionListModel` + `SettingsListRenderer`.
- Mouse hit-testing via `settingsListCellAt` → `onCell(key, cellId)`; Enter triggers the first
visible cell of the selected row; `ScrollingUtil.installActions`.
- `update(items)`, `filter(query)` (client-side via `ModelSearch.matches(query, title)`),
`setBusy(b)` (disables the list; renderer drops cells while `!list.isEnabled`), `move(step)`,
`primary()`, selection preserved by `key`, configurable `emptyText`.
### 5. `settings/base/SettingsListPanel.kt`
`SettingsPanel(), Disposable` — the generic page shell (generalized `ProvidersSettingsUi` minus
OAuth/dialogs):
- Header (`content` NORTH): an `ActionToolbar` (optional `Refresh` + `extraActions()`), an optional
`headerExtras()` component (combo/toggle), and a `SearchTextField`; list hosted via `setContent`.
- Request-guarded async (`request`/`active(id)`/`busy`, ported verbatim from providers):
`reload()` → `fetch()` → `view.update(...)` on EDT, with `showProgress`/`showError`/`clearProgress`.
- Open/abstract hooks:
- `suspend fun fetch(): List<SettingsListItem>`
- `fun onCell(key: String, cellId: String)`
- `open fun extraActions(): List<AnAction> = emptyList()`
- `open fun headerExtras(): JComponent? = null`
- `open fun searchPlaceholder(): String`, `open fun emptyText(): String`
- `open fun showRefresh(): Boolean = true`
- `@RequiresEdt` everywhere that touches Swing; `checkEdt()` guard (same discipline as providers).
A small `SettingsToolbarAction(text, description, icon, enabled, action)` (generalized
`ProviderToolbarAction`) backs toolbar buttons.
---
## Providers refactor (adopt shared)
- `ProviderListRow : SettingsListItem`:
- `title = provider.name`, `description = providerDescription(provider)`,
`icon = providerIcon(provider)`, `section` (existing), `disabled` (existing).
- `cells` mapped from `ProviderListAction`: label via existing `text(action)`,
`enabled = enabled(action)` (env source can't disconnect → `enabled=false`),
`alwaysVisible = (action == DISCONNECT && connected)`.
- Optional badges: `Custom` when `source == "custom"`, `Env` when `source == "env"` (nice-to-have;
can ship empty initially).
- Keep `ProviderListAction` enum + `providerListRows`/`providerActions`/section bucketing in the
providers package (provider domain logic stays).
- `ProvidersContent` → use `SettingsListView` (`onCell` maps cellId → `ProviderListAction` →
connect/oauth/disconnect/enable). Section bucketing stays at fetch time; client-side filter applies
on top and section titles recompute from the filtered list.
- `ProvidersSettingsUi` → extend `SettingsListPanel`:
- `fetch()` calls `KiloProviderService.state(dir)`, stores `state` (needed for dialogs/auth), maps to
rows.
- `extraActions()` = the `Add custom provider` action; `onCell` dispatches provider actions.
- Keep all provider-specific overlay logic (OAuth device panel, countdown timer, `ApiKeyDialog`,
`CustomProviderDialog`, cancel) — these already live on `SettingsOverlayPanel`/`SettingsPanel`.
- Renderer/hit-testing: delete `ProviderListRenderer` and use `SettingsListRenderer` +
`settingsListCellAt/Bounds/VisibleCells`.
- **Behavior to preserve** (and assert in tests): connected row shows Disconnect even when not
selected; unselected unconnected rows show no actions; env disconnect is disabled and not
hit-testable; disabled rows show nothing; section ordering Connected→Popular→All; reload request
guard; dispose cancels.
- **Test migration:** `ProvidersSettingsUiTest` calls `ProviderListRenderer.actionAt/actionBounds/
visibleActions` and asserts `ProviderListRow.actions`. Update these to the shared
`settingsListCellAt/...` functions and `SettingsListCell`s, keeping every behavioral assertion.
- **Fallback (if full-shell adoption risks provider behavior):** keep `ProvidersSettingsUi`/
`ProvidersContent` bespoke but adopt the shared **renderer + row interface + hit-testing + badge**
only. Documented as the lower-risk fallback; prefer full adoption.
---
## Per-page refactor (agent-behavior)
Each page becomes a thin `SettingsListPanel` subclass. The configurable bases switch to the
providers-style shell (own scroll), so update `AgentBehaviorConfigurableBase` to dispose the panel
when it is `Disposable` and let converted children return `scrollReadyShell() = false` (MCP, still
`BaseContentPanel`, keeps the default scroll shell).
### A. Agents — `AgentsConfigurable` / `AgentsSettingsUi`
- `headerExtras()` = a `SettingsRow` with the **Default Agent** combo (existing `default_agent`
config via `AgentBehaviorPage.modified/applyDraft/resetDraft`).
- `fetch()` = `agents(dir)` → rows: `title = displayName ?: name`, `description`, badges
`[mode]` + `Custom` (when `native != true`) + `Hidden` (when `hidden`) + `Deprecated` (when
`deprecated`); `cells = [Remove]` only when custom.
- `onCell("remove")` → `removeAgent(dir, name)` then `reload()`.
- `extraActions()` empty; `showRefresh() = true`; filter by title.
### B. Rules — `RulesConfigurable` / `RulesSettingsUi`
- `headerExtras()` = `SettingsRow` with the **Claude Code compat** `SettingsToggle`
(`claudeCodeCompat()`/`setClaudeCodeCompat()`), kept as today.
- `fetch()` = map `config.instructions` (draft) to rows: `title = instruction`, no badges,
`cells = [Remove]` (selected-only). Filter by text.
- `extraActions()` = an **Add** toolbar action (input dialog → append to draft → `reload()`), since
Rules retains Add (decision 2). `showRefresh()` optional (config-only); include reset via the
standard Reset.
- `AgentBehaviorPage`: `modified/applyDraft/resetDraft` persist `instructions` via `ConfigPatchDto`
(unchanged).
### C. Skills — `SkillsConfigurable` / `SkillsSettingsUi`
- One unified list with three sections (via `section` on each row):
- **Skill Folder Paths** — rows from `config.skills.paths` (draft), `cells = [Remove]`
(selected-only). No Add (decision 1).
- **Skill URLs** — rows from `config.skills.urls` (draft), `cells = [Remove]`. No Add.
- **Discovered Skills** — `fetch()` = `skills(dir)`; badge `Built-in` (`location == "builtin"`)
else `Custom`; `cells = [Remove]` only for custom → `removeSkill(dir, location)` + `reload()`.
- `onCell` routes by section/key: path/url removes mutate the draft and re-sync; discovered remove
calls the service. `AgentBehaviorPage` persists `SkillsPatchDto(paths, urls)`.
- `showRefresh() = true` (reloads discovered + re-reads config draft baseline).
### D. Workflows — `WorkflowsConfigurable` / `WorkflowsSettingsUi`
- Read-only. `fetch()` = `commands(dir)` → rows: `title = "/" + name`, `description`, badge `[source]`,
no cells. `showRefresh() = true`; filter by title; empty state via `settings.agentBehavior.empty`.
- No `AgentBehaviorPage` (nothing to apply).
---
## Badges
| Page | Badge(s) | Source field | Tone |
|---|---|---|---|
| Agents | mode (`primary`/`subagent`/`all`) | `mode` | NEUTRAL |
| Agents | `Custom` | `native != true` | ACCENT |
| Agents | `Hidden` | `hidden == true` | NEUTRAL |
| Agents | `Deprecated` | `deprecated == true` | WARNING |
| Skills | `Built-in` / `Custom` | `location == "builtin"` | NEUTRAL / ACCENT |
| Workflows | source (`command`/`mcp`/`skill`) | `source` | NEUTRAL |
| Providers (optional) | `Custom` / `Env` | `source` | ACCENT / NEUTRAL |
Badge text comes from new bundle keys; colors are theme-derived (`UiStyle.Colors`), never hardcoded.
---
## Configurable wiring
- `AgentBehaviorConfigurableBase`: keep `AgentBehaviorPage` delegation; add disposal of the panel when
it implements `Disposable`; converted children override `scrollReadyShell() = false`.
- XML registration in `kilo.jetbrains.frontend.xml` is unchanged (same five children).
---
## i18n
Add to `frontend/src/main/resources/messages/KiloBundle.properties` (English; locales fall back):
- Filter placeholders: `settings.agentBehavior.agents.search`, `.skills.search`, `.workflows.search`,
`.rules.search`.
- Toolbar: `settings.agentBehavior.refresh` / `.refresh.description`; `settings.agentBehavior.rules.add`
(+ add-dialog title/prompt). Reuse `settings.agentBehavior.remove`, `settings.agentBehavior.empty`.
- Badges: `settings.agentBehavior.badge.custom`, `.builtin`, `.hidden`, `.deprecated`; agent mode and
workflow source can reuse the raw value or add `settings.agentBehavior.badge.mode.*` /
`.source.*` keys.
- Keep existing section titles (`agents.available`, `skills.paths`, `skills.urls`, `skills.discovered`,
`rules.*`). Add a workflows section title if needed.
(Confirm whether CI requires the new keys in every `KiloBundle_<locale>.properties`; if so, mirror the
English value as the fallback. Otherwise English-only is fine.)
---
## Testing
- New fake `frontend/src/test/.../testing/FakeAgentBehaviorRpcApi.kt` (implements
`KiloAgentBehaviorRpcApi`), injected via `KiloAgentBehaviorService(cs, fake)` + `replaceService`
(mirrors `FakeProviderRpcApi`). Seed `KiloAppService` state for config-backed values (default agent,
instructions, skills config) following the existing settings-test setup.
- Shared component tests (mirror `ProvidersSettingsUiTest` helpers — `edt {}`, `flushUntil`, component
walk):
- `SettingsListRendererTest`: badges render per `item.badges`; cells visible only when selected;
`alwaysVisible` cell visible unselected; disabled rows show no cells; `enabled=false` cell not
hit-testable; section titles; icon hidden when null.
- `SettingsListViewTest` / `SettingsListPanelTest`: client-side filter, selection preserved by key,
Enter triggers primary cell, `onCell` dispatch, refresh reload, busy disables.
- Page tests: `AgentsSettingsUiTest`, `SkillsSettingsUiTest`, `WorkflowsSettingsUiTest`,
`RulesSettingsUiTest` — rows + badges, remove only when selected, remove calls the service/mutates
draft + reloads, filter, refresh, default-agent apply, Claude toggle, rules Add.
- Update `ProvidersSettingsUiTest` to the shared renderer/hit-testing/cell APIs, preserving every
behavioral assertion (connected-disconnect-always-visible, env-disabled, sections, reload guard,
dispose).
---
## File-by-file / task checklist
**New shared (`frontend/.../settings/base/`)**
- [ ] `SettingsListModel.kt` (interface, `SettingsBadge`, `SettingsListCell`, helpers).
- [ ] `SettingsListRenderer.kt`.
- [ ] `SettingsListView.kt`.
- [ ] `SettingsListPanel.kt` (+ `SettingsToolbarAction`).
- [ ] `client/ui/BadgeLabel.kt` (shared).
**Providers (adopt shared)**
- [ ] `ProviderListRows.kt`: `ProviderListRow : SettingsListItem` (+ cell/badge mapping).
- [ ] Replace `ProviderListRenderer.kt` usage with `SettingsListRenderer` + shared hit-testing
(delete the file or keep a thin façade only if needed for tests).
- [ ] `ProvidersSettingsUi.kt`/`ProvidersContent`: build on `SettingsListPanel`/`SettingsListView`;
keep OAuth/dialog/custom-provider logic.
- [ ] Update `ProvidersSettingsUiTest.kt` to shared APIs.
**Agent-behavior pages (`frontend/.../settings/agentbehavior/`)**
- [ ] `AgentBehaviorConfigurableBase.kt`: dispose `Disposable` panels; allow `scrollReadyShell=false`.
- [ ] `AgentsConfigurable.kt` / `AgentsSettingsUi` → `SettingsListPanel` (default-agent header + list).
- [ ] `RulesConfigurable.kt` / `RulesSettingsUi` → `SettingsListPanel` (Claude toggle header +
instructions list + Add).
- [ ] `SkillsConfigurable.kt` / `SkillsSettingsUi` → `SettingsListPanel` (paths/urls/discovered
sections, remove-only).
- [ ] `WorkflowsConfigurable.kt` / `WorkflowsSettingsUi` → `SettingsListPanel` (read-only).
- [ ] Remove now-unused `SettingsListEditor` usages where add-editors are dropped (keep the class if
still used elsewhere; otherwise delete).
**i18n & tests**
- [ ] New `settings.agentBehavior.*` keys in `KiloBundle.properties` (+ locale fallbacks if CI requires).
- [ ] `FakeAgentBehaviorRpcApi.kt` + the renderer/view/panel/page tests listed above.
**Out of scope**
- MCP page conversion (sibling list; not requested) — note as optional follow-up using the same
shared classes.
- Any new create/edit of agents/rules/skills/workflows; re-adding skill paths/urls Add.
---
## Verification
From `packages/kilo-jetbrains/` (Java 21 required):
- `./gradlew typecheck` (or `bun run typecheck`).
- `./gradlew test` (or target the new/updated tests).
- Manual: `./gradlew runIde` → Settings → Tools → Kilo Code → Agent Behavior. Confirm each page shows a
filter field + refresh toolbar, inline Remove appears only on the selected row, badges render
(custom/built-in/mode/source), and Providers still behaves identically (OAuth, custom provider,
connected-disconnect).
## Risks / notes
- **Provider adoption is the highest-risk step** (large test suite, OAuth/dialog overlays, busy/section
logic). Do it last; if behavior can't be cleanly preserved on the full generic shell, fall back to
adopting only the shared renderer + row interface + hit-testing + badge in Providers.
- **Skills three-section list** + remove-only paths/urls is an intentional interim state; flag in the
PR description.
- **EDT discipline**: port the providers `@RequiresEdt`/`checkEdt`/request-guard patterns exactly into
the shared panel to avoid threading regressions.
@@ -1,87 +0,0 @@
# Refactor JetBrains Badge Styles
## Goal
Centralize all JetBrains badge color choices under `UiStyle.Badge`, replace settings badge tones with explicit badge style objects, and update badge renderers to consume those objects instead of separate background/foreground color functions.
## Proposed Design
1. Add `UiStyle.Badge` in `frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt`.
2. Define a nested style contract, for example `UiStyle.Badge.Style`, with `bg(): Color` and `fg(): Color`.
3. Add nested objects with the correctly spelled names:
- `Primary`: current activity/accent badge palette, blue background and white foreground.
- `Secondary`: current grey settings badge palette, using `Badge.background` / `Badge.foreground` with the current fallbacks.
- `Free`: current free-model badge palette from `ModelText.freeBg()` plus the current free badge foreground.
- `Alert`: current running badge palette.
4. Remove badge-specific color functions from `UiStyle.Colors`: `badgeBg`, `badgeFg`, `settingsBadgeBg`, `settingsBadgeFg`, `runningBadgeBg`, `runningBadgeFg`, `activityBadgeBg`, `activityBadgeFg`, `warningBadgeBg`, and `warningBadgeFg`.
5. Keep non-badge helpers such as `warningLabelForeground`, `bright`, and `blend` where still used.
## Implementation Steps
1. Update `FilledBadgeIcon` to accept a `UiStyle.Badge.Style` instead of raw `bg` and `fg` colors.
- Paint by calling `style.bg()` and `style.fg()`.
- Keep `text` available for existing tests.
2. Replace settings badge tone with explicit styles.
- In `settings/base/SettingsListModel.kt`, remove `SettingsBadgeTone`.
- Change `SettingsBadge` to `data class SettingsBadge(val text: String, val style: UiStyle.Badge.Style = UiStyle.Badge.Secondary)`.
- In `settings/base/SettingsListRenderer.kt`, replace the `when (badge.tone)` color mapping with `FilledBadgeIcon(badge.text, badge.style)`.
3. Update settings call sites.
- `AgentsConfigurable.kt`: remove `SettingsBadgeTone`; use `UiStyle.Badge.Alert` for deprecated badges; default secondary for subagent/custom/hidden unless a different style is intentionally requested.
- `SkillsConfigurable.kt`: remove `SettingsBadgeTone`; use `UiStyle.Badge.Primary` for the custom badge and default secondary for builtin.
- `WorkflowsConfigurable.kt` and `ProviderListRows.kt`: keep default secondary badges.
4. Move free-model badge color ownership into `UiStyle.Badge.Free`.
- In `ModelPickerRenderer.kt`, construct `FilledBadgeIcon(ModelText.freeLabel(), UiStyle.Badge.Free)`.
- In `ModelPicker.kt`, remove `ModelText.freeBg()` if it is no longer used.
- Remove now-unused `JBColor` imports from affected files.
5. Update session activity badges.
- In `SessionActivityKind.kt`, replace `bg()` / `fg()` with a `style()` function returning `UiStyle.Badge.Alert` for `RUNNING` and `UiStyle.Badge.Primary` for `LOGIN_REQUIRED`, `PERMISSION`, `PLAN`, and `QUESTION`.
- Update `HistoryListRenderer.kt` and `RecentsList.kt` to pass `it.style()` to `FilledBadgeIcon`.
6. Update account and mode badges.
- In `SessionAccountOverlay.kt`, replace balance badge raw colors with `UiStyle.Badge.Secondary`.
- In `ModePickerRenderer.kt`, convert the deprecated mode badge to use `FilledBadgeIcon(KiloBundle.message("mode.picker.deprecated"), UiStyle.Badge.Alert)` so it uses the centralized alert badge style.
- Remove `RoundedLineBorder` and border/color setup that only existed for the old outlined deprecated badge.
7. Clean up imports and verify there are no remaining call sites for old badge color functions or `SettingsBadgeTone`.
## Files Expected To Change
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/FilledBadgeIcon.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListModel.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListRenderer.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agentbehavior/AgentsConfigurable.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agentbehavior/SkillsConfigurable.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionActivityKind.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/empty/RecentsList.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/account/SessionAccountOverlay.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt`
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePickerRenderer.kt`
## Tests And Verification
1. Run focused searches after editing:
- No `SettingsBadgeTone` remains.
- No `UiStyle.Colors.*Badge*` badge color functions remain.
- `FilledBadgeIcon(` call sites all pass a `UiStyle.Badge.*` style.
2. Run the smallest relevant JetBrains check from `packages/kilo-jetbrains/`:
- `bun run typecheck`
3. If typecheck is insufficient or changed tests fail locally, run focused frontend tests covering these renderers:
- `UiStyleTest`
- `ModelPickerTest`
- `ModePickerTest`
- `HistoryControllerTest`
- `EmptySessionPanelTest`
- settings UI tests that assert badge text
## Notes
- The object should be named `Secondary`, not `Seconday` / `Secondaty`.
- This is JetBrains-only UI code, so no `kilocode_change` markers are needed.
- This is an internal style refactor and does not require SDK regeneration or CLI artifact refresh.
+7 -1
View File
@@ -606,10 +606,16 @@ Settings UI has reusable primitives in `frontend/src/main/kotlin/ai/kilocode/cli
### Lists And Add/Remove Collections
- For add/remove/edit collections, use the shared list infrastructure: `SettingsListPanel`, `SettingsListView`, `SettingsListItem`, `SettingsListCell`, `SettingsListSelection`, and `SettingsToolbarAction` where applicable.
- When a setting is a list of values that can be added or removed inline, represent it with common list/editor primitives such as `SettingsListEditor`, toolbar actions, and in-place cells/buttons as needed.
- When a setting is a list of values that can be added or removed inline, represent it with common list/editor primitives, toolbar actions, and in-place cells/buttons as needed.
- Do not build a bespoke set of Swing components for each add/remove list situation.
- Prefer list action cells (`SettingsListCell`) for row-local actions like edit/delete and toolbar actions for global add/import/refresh actions.
### Settings Test Coverage Pattern
- Each settings page that writes state needs a fake-RPC frontend test that proves UI interactions call the expected client service/RPC method.
- Each backend-backed settings write path needs a `*RpcApiImpl` or manager test against `MockCliServer` that asserts the exact CLI HTTP body and that a subsequent reload observes the persisted value.
- Navigation-only settings pages should still have `BasePlatformTestCase` coverage for rendered child links, stable child IDs, and inert `isModified`/`apply` behavior.
## Session Component
The chat session feature uses a three-layer Model / Controller / View architecture. All files live under
@@ -9,6 +9,7 @@ import ai.kilocode.rpc.dto.AgentConfigDto
import ai.kilocode.rpc.dto.ChatEventDto
import ai.kilocode.rpc.dto.CloudSessionDto
import ai.kilocode.rpc.dto.CloudSessionListDto
import ai.kilocode.rpc.dto.CommandDto
import ai.kilocode.rpc.dto.ConfigDto
import ai.kilocode.rpc.dto.ConfigPatchDto
import ai.kilocode.rpc.dto.ConfigUpdateDto
@@ -21,6 +22,7 @@ import ai.kilocode.rpc.dto.MessageErrorDto
import ai.kilocode.rpc.dto.MessageTimeDto
import ai.kilocode.rpc.dto.MessageWithPartsDto
import ai.kilocode.rpc.dto.McpConfigDto
import ai.kilocode.rpc.dto.McpStatusDto
import ai.kilocode.rpc.dto.ModelDto
import ai.kilocode.rpc.dto.ModelLimitDto
import ai.kilocode.rpc.dto.ModelSelectionDto
@@ -50,6 +52,7 @@ import ai.kilocode.rpc.dto.SessionDto
import ai.kilocode.rpc.dto.SessionStatusDto
import ai.kilocode.rpc.dto.SessionSummaryDto
import ai.kilocode.rpc.dto.SessionTimeDto
import ai.kilocode.rpc.dto.SkillDto
import ai.kilocode.rpc.dto.SkillsConfigDto
import ai.kilocode.rpc.dto.TodoDto
import ai.kilocode.rpc.dto.TodoViewDto
@@ -595,6 +598,70 @@ object KiloCliDataParser {
)
}
fun parseAgentRemovable(raw: String): Map<String, Boolean> =
raw.array().mapNotNull { item ->
val obj = item.obj() ?: return@mapNotNull null
val name = obj.str("name") ?: return@mapNotNull null
name to removable(obj)
}.toMap()
fun parseAgentBehaviorSkills(raw: String): List<SkillDto> =
raw.array().mapNotNull { item ->
val obj = item.obj() ?: return@mapNotNull null
val name = obj.str("name") ?: return@mapNotNull null
val location = obj.str("location") ?: return@mapNotNull null
SkillDto(name = name, description = obj.str("description"), location = location)
}
fun parseAgentBehaviorCommands(raw: String): List<CommandDto> =
raw.array().mapNotNull { item ->
val obj = item.obj() ?: return@mapNotNull null
val name = obj.str("name") ?: return@mapNotNull null
CommandDto(
name = name,
description = obj.str("description"),
source = obj.str("source"),
hints = obj["hints"].arr()?.mapNotNull { it.jsonPrimitive.contentOrNull } ?: emptyList(),
template = obj.str("template"),
)
}
fun parseMcpStatus(raw: String): List<McpStatusDto> {
val root = runCatching { json.parseToJsonElement(raw) }.getOrNull() ?: return emptyList()
return when (root) {
is JsonArray -> root.mapNotNull { mcpStatus(it) }
is JsonObject -> root.mapNotNull { (name, item) -> mcpStatus(item, name) }
else -> emptyList()
}
}
private fun String.array(): JsonArray {
val root = runCatching { json.parseToJsonElement(this) }.getOrNull()
return when (root) {
is JsonArray -> root
is JsonObject -> root["data"] as? JsonArray ?: JsonArray(emptyList())
else -> JsonArray(emptyList())
}
}
private fun mcpStatus(item: JsonElement, fallback: String? = null): McpStatusDto? {
val obj = item.obj() ?: return null
val name = obj.str("name") ?: fallback ?: return null
return McpStatusDto(
name = name,
status = obj.str("status") ?: obj.str("state") ?: "unknown",
error = obj.str("error"),
)
}
private fun removable(obj: JsonObject): Boolean {
if (obj.str("native") == "true") return false
val opts = obj["options"].obj()
if (obj.str("source") == "organization" || opts?.str("source") == "organization") return false
if (opts?.containsKey("reference") == true || opts?.containsKey("resolved") == true) return false
return true
}
/**
* Extract the `state` directory path from a `/path` response.
* Returns `null` when the field is missing, not a JSON string, or the JSON is malformed.
@@ -10,51 +10,37 @@ import ai.kilocode.rpc.KiloAgentBehaviorRpcApi
import ai.kilocode.rpc.dto.AgentCreateDto
import ai.kilocode.rpc.dto.AgentDetailDto
import ai.kilocode.jetbrains.api.model.AgentBuilderSaveRequest
import ai.kilocode.rpc.dto.CommandDto
import ai.kilocode.rpc.dto.ConfigPatchDto
import ai.kilocode.rpc.dto.McpConfigDto
import ai.kilocode.rpc.dto.McpServerConfigDto
import ai.kilocode.rpc.dto.McpStatusDto
import ai.kilocode.rpc.dto.PermissionRuleItemDto
import ai.kilocode.rpc.dto.SkillDto
import com.intellij.openapi.components.service
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
class KiloAgentBehaviorRpcApiImpl : KiloAgentBehaviorRpcApi {
class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = null) : KiloAgentBehaviorRpcApi {
companion object {
private val LOG = KiloLog.create(KiloAgentBehaviorRpcApiImpl::class.java)
private val JSON = "application/json".toMediaType()
private val PARSER = Json { ignoreUnknownKeys = true }
private val saved = ConcurrentHashMap<String, SavedMcp>()
private val port = AtomicInteger(-1)
}
private val app: KiloBackendAppService get() = service()
private val app: KiloBackendAppService get() = backend ?: service()
override suspend fun agents(directory: String): List<AgentDetailDto> {
app.requireReady()
val api = app.api ?: throw IllegalStateException("Kilo API is unavailable")
val raw = get(directory, "/agent").array().mapNotNull { item ->
val obj = item as? JsonObject ?: return@mapNotNull null
val name = obj.string("name") ?: return@mapNotNull null
name to obj
}.toMap()
val removable = KiloCliDataParser.parseAgentRemovable(request(directory, "/agent", null))
return withContext(Dispatchers.IO) { api.appAgents(directory = directory) }.map { item ->
AgentDetailDto(
name = item.name,
@@ -62,7 +48,7 @@ class KiloAgentBehaviorRpcApiImpl : KiloAgentBehaviorRpcApi {
description = item.description,
mode = item.mode.value,
native = item.native,
removable = removable(raw[item.name]),
removable = removable[item.name] ?: false,
hidden = item.hidden,
deprecated = item.deprecated,
permission = rules(item.permission),
@@ -70,12 +56,7 @@ class KiloAgentBehaviorRpcApiImpl : KiloAgentBehaviorRpcApi {
}
}
override suspend fun skills(directory: String): List<SkillDto> = get(directory, "/skill").array().mapNotNull { item ->
val obj = item.jsonObject
val name = obj.string("name") ?: return@mapNotNull null
val location = obj.string("location") ?: return@mapNotNull null
SkillDto(name = name, description = obj.string("description"), location = location)
}
override suspend fun skills(directory: String) = KiloCliDataParser.parseAgentBehaviorSkills(request(directory, "/skill", null))
override suspend fun removeSkill(directory: String, location: String): Boolean =
post(directory, "/kilocode/skill/remove", JsonObject(mapOf("location" to JsonPrimitive(location))))
@@ -99,25 +80,10 @@ class KiloAgentBehaviorRpcApiImpl : KiloAgentBehaviorRpcApi {
return true
}
override suspend fun commands(directory: String): List<CommandDto> = get(directory, "/command").array().mapNotNull { item ->
val obj = item.jsonObject
val name = obj.string("name") ?: return@mapNotNull null
CommandDto(
name = name,
description = obj.string("description"),
source = obj.string("source"),
template = obj.string("template"),
)
}
override suspend fun commands(directory: String) = KiloCliDataParser.parseAgentBehaviorCommands(request(directory, "/command", null))
override suspend fun mcpStatus(directory: String): List<McpStatusDto> = get(directory, "/mcp").let { root ->
val items = when (root) {
is JsonArray -> root.mapNotNull(::mcp)
is JsonObject -> root.mapNotNull { (name, item) -> mcp(item, name) }
else -> emptyList()
}
override suspend fun mcpStatus(directory: String) = KiloCliDataParser.parseMcpStatus(request(directory, "/mcp", null)).also { items ->
LOG.info("MCP status returned dir=$directory count=${items.size}")
items
}
override suspend fun mcpConfig(directory: String): Map<String, McpServerConfigDto> {
@@ -168,11 +134,6 @@ class KiloAgentBehaviorRpcApiImpl : KiloAgentBehaviorRpcApi {
return value
}
private suspend fun get(directory: String, path: String): JsonElement {
val raw = request(directory, path, null)
return PARSER.parseToJsonElement(raw)
}
private suspend fun post(directory: String, path: String, body: JsonObject = JsonObject(emptyMap())): Boolean {
request(directory, path, body)
return true
@@ -217,25 +178,8 @@ class KiloAgentBehaviorRpcApiImpl : KiloAgentBehaviorRpcApi {
}
}
private fun mcp(item: JsonElement, fallback: String? = null): McpStatusDto? {
val obj = item.jsonObject
val name = obj.string("name") ?: fallback ?: return null
return McpStatusDto(
name = name,
status = obj.string("status") ?: obj.string("state") ?: "unknown",
error = obj.string("error"),
)
}
private fun JsonElement.array(): JsonArray = when (this) {
is JsonArray -> this
is JsonObject -> this["data"] as? JsonArray ?: JsonArray(emptyList())
else -> JsonArray(emptyList())
}
private fun JsonObject.string(key: String): String? = (this[key] as? JsonPrimitive)?.contentOrNull
private fun withSavedMcp(directory: String, items: Map<String, McpServerConfigDto>): Map<String, McpServerConfigDto> = buildMap {
syncSaved()
putAll(items)
for (item in saved.values) {
if (item.scope == "workspace" && item.directory != directory) continue
@@ -245,6 +189,7 @@ class KiloAgentBehaviorRpcApiImpl : KiloAgentBehaviorRpcApi {
}
private fun saveMcpOverride(directory: String, name: String, scope: String, config: McpConfigDto?) {
syncSaved()
val key = mcpKey(if (scope == "workspace") directory else "", name)
if (config == null) {
saved.remove(key)
@@ -260,14 +205,10 @@ class KiloAgentBehaviorRpcApiImpl : KiloAgentBehaviorRpcApi {
private fun mcpKey(directory: String, name: String): String = "$directory\u0000$name"
private fun removable(obj: JsonObject?): Boolean {
if (obj == null) return false
if ((obj["native"] as? JsonPrimitive)?.contentOrNull == "true") return false
val source = obj.string("source")
val opts = obj["options"] as? JsonObject
if (source == "organization" || opts?.string("source") == "organization") return false
if (opts?.containsKey("reference") == true || opts?.containsKey("resolved") == true) return false
return true
private fun syncSaved() {
val current = runCatching { app.port }.getOrDefault(-1)
val prev = port.getAndSet(current)
if (prev != current) saved.clear()
}
private fun prop(obj: Any, name: String): Any? {
@@ -7,6 +7,8 @@ import ai.kilocode.backend.rpc.appStateDto
import ai.kilocode.backend.testing.FakeCliServer
import ai.kilocode.backend.testing.MockCliServer
import ai.kilocode.backend.testing.TestLog
import ai.kilocode.rpc.dto.AgentConfigPatchDto
import ai.kilocode.rpc.dto.ConfigPatchDto
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -132,6 +134,35 @@ class KiloBackendAppServiceTest {
assertEquals("claude-4", svc.config!!.model)
}
@Test
fun `update config patches model selections and reloads`() = runBlocking {
val svc = create()
svc.connect()
ready(svc)
val state = svc.updateConfig(ConfigPatchDto(
values = linkedMapOf(
"model" to "openai/gpt-5",
"small_model" to "openai/gpt-5-mini",
"subagent_model" to "anthropic/claude",
"subagent_variant" to "high",
),
agents = linkedMapOf("code" to AgentConfigPatchDto(model = "google/gemini", variant = "fast")),
))
assertEquals(
"{\"model\":\"openai/gpt-5\",\"small_model\":\"openai/gpt-5-mini\",\"subagent_model\":\"anthropic/claude\",\"subagent_variant\":\"high\",\"agent\":{\"code\":{\"model\":\"google/gemini\",\"variant\":\"fast\"}}}",
mock.lastConfigPatchBody,
)
val cfg = appStateDto(state).config
assertEquals("openai/gpt-5", cfg?.model)
assertEquals("openai/gpt-5-mini", cfg?.smallModel)
assertEquals("anthropic/claude", cfg?.subagentModel)
assertEquals("high", cfg?.subagentVariant)
assertEquals("google/gemini", cfg?.agent?.get("code")?.model)
assertEquals("fast", svc.config?.agent?.get("code")?.variant)
}
@Test
fun `ready dto maps model config`() = runBlocking {
mock.config = """{"model":"openai/gpt","agent":{"plan":{"model":"anthropic/claude","variant":"high"}}}"""
@@ -5,6 +5,7 @@ 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.ProviderConnectDto
import ai.kilocode.rpc.dto.ProviderDisconnectDto
import ai.kilocode.rpc.dto.ProviderEnableDto
import kotlinx.coroutines.async
@@ -58,6 +59,32 @@ class KiloBackendProviderSettingsManagerTest {
assertEquals(0, mock.requestCount("/global/dispose"))
}
@Test
fun `connecting provider stores credentials and reloads connected state`() = runBlocking {
mock.providers = """{
"all":[{"id":"openai","name":"OpenAI","source":"custom","models":{}}],
"default":{},
"connected":[],
"failed":[]
}""".trimIndent()
mock.providersAfterAuthPut = """{
"all":[{"id":"openai","name":"OpenAI","source":"custom","models":{}}],
"default":{},
"connected":["openai"],
"failed":[]
}""".trimIndent()
val manager = manager()
mock.resetCounts()
val result = manager.connect(ProviderConnectDto("/test", "openai", "sk-test", mapOf("baseURL" to "https://api.openai.com/v1")))
assertNull(result.error)
assertContains(mock.lastAuthPutBody.orEmpty(), "\"key\":\"sk-test\"")
assertContains(mock.lastAuthPutBody.orEmpty(), "\"baseURL\":\"https://api.openai.com/v1\"")
assertEquals(listOf("openai"), result.state.connected)
assertEquals(1, mock.requestCount("/global/dispose"))
}
@Test
fun `disconnecting openai compatible custom provider deletes config and auth`() = runBlocking {
mock.config = """{
@@ -0,0 +1,142 @@
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.AgentCreateDto
import ai.kilocode.rpc.dto.McpConfigDto
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.withTimeout
import kotlin.test.AfterTest
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class KiloAgentBehaviorRpcApiImplTest {
private val mock = MockCliServer()
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
@AfterTest
fun tearDown() {
scope.cancel()
mock.close()
}
@Test
fun `agents merges SDK data with removable flags`() = runBlocking {
mock.agents = """[
{"name":"custom","displayName":"Custom","description":"Editable","mode":"primary","native":false,"source":"project","options":{}},
{"name":"org","displayName":"Org","mode":"subagent","options":{"source":"organization"}},
{"name":"builtin","displayName":"Builtin","mode":"all","native":true,"options":{}}
]""".trimIndent()
val rpc = rpc()
val agents = rpc.agents("/test")
assertEquals(listOf("custom", "org", "builtin"), agents.map { it.name })
assertEquals(true, agents.single { it.name == "custom" }.removable)
assertEquals(false, agents.single { it.name == "org" }.removable)
assertEquals(false, agents.single { it.name == "builtin" }.removable)
}
@Test
fun `create and remove agent call CLI endpoints`() = runBlocking {
val rpc = rpc()
assertTrue(rpc.createAgent("/test project", AgentCreateDto(
name = "custom",
prompt = "Use the project conventions",
mode = "subagent",
description = "Project helper",
scope = "global",
)))
assertEquals("PUT", mock.lastAgentBuilderMethod)
assertContains(mock.lastAgentBuilderPath.orEmpty(), "/agent-builder/custom")
assertContains(mock.lastAgentBuilderPath.orEmpty(), "directory=%2Ftest%20project")
assertContains(mock.lastAgentBuilderBody.orEmpty(), "\"prompt\":\"Use the project conventions\"")
assertContains(mock.lastAgentBuilderBody.orEmpty(), "\"scope\":\"global\"")
assertTrue(rpc.removeAgent("/test", "custom"))
assertEquals("{\"name\":\"custom\"}", mock.lastAgentRemoveBody)
mock.agentRemoveStatus = 400
val err = assertFailsWith<RuntimeException> {
rpc.removeAgent("/test", "missing")
}
assertContains(err.message.orEmpty(), "HTTP 400")
}
@Test
fun `mcp config writes global and workspace patches`() = runBlocking {
mock.config = """{"mcp":{"global":{"type":"local","command":["node","g.js"]}}}"""
mock.workspaceConfig = """{"mcp":{"workspace":{"type":"remote","url":"https://workspace.test"}}}"""
val rpc = rpc()
val initial = rpc.mcpConfig("/test dir")
assertEquals(setOf("global", "workspace"), initial.keys)
assertEquals("global", initial["global"]?.scope)
assertEquals("workspace", initial["workspace"]?.scope)
assertTrue(rpc.saveMcp("/test dir", "global-added", "global", McpConfigDto(
type = "local",
command = listOf("node", "server.js"),
environment = mapOf("TOKEN" to "x"),
)))
assertContains(mock.lastConfigPatchBody.orEmpty(), "\"global-added\"")
assertContains(mock.lastConfigPatchBody.orEmpty(), "\"environment\":{\"TOKEN\":\"x\"}")
assertEquals("local", rpc.mcpConfig("/test dir")["global-added"]?.config?.type)
assertTrue(rpc.saveMcp("/test dir", "workspace-added", "workspace", McpConfigDto(
type = "remote",
url = "https://mcp.example.test",
headers = mapOf("Authorization" to "Bearer t"),
)))
assertEquals("/config?directory=%2Ftest+dir", mock.lastWorkspaceConfigPatchPath)
assertContains(mock.lastWorkspaceConfigPatchBody.orEmpty(), "\"workspace-added\"")
assertEquals("workspace", rpc.mcpConfig("/test dir")["workspace-added"]?.scope)
assertTrue(rpc.saveMcp("/test dir", "workspace-added", "workspace", null))
assertContains(mock.lastWorkspaceConfigPatchBody.orEmpty(), "\"workspace-added\":null")
assertFalse(rpc.mcpConfig("/test dir").containsKey("workspace-added"))
}
@Test
fun `mcp status and actions call CLI endpoints`() = runBlocking {
mock.mcp = """{"local":{"status":"connected"},"remote":{"state":"disconnected","error":"missing auth"}}"""
val rpc = rpc()
val status = rpc.mcpStatus("/test")
assertEquals("connected", status.single { it.name == "local" }.status)
assertEquals("missing auth", status.single { it.name == "remote" }.error)
assertTrue(rpc.mcpConnect("/test", "local server"))
assertContains(mock.lastMcpActionPath.orEmpty(), "/mcp/local%20server/connect")
assertTrue(rpc.mcpDisconnect("/test", "local server"))
assertContains(mock.lastMcpActionPath.orEmpty(), "/mcp/local%20server/disconnect")
assertTrue(rpc.mcpAuthenticate("/test", "local server"))
assertContains(mock.lastMcpActionPath.orEmpty(), "/mcp/local%20server/auth/authenticate")
}
private suspend fun rpc(): KiloAgentBehaviorRpcApiImpl = KiloAgentBehaviorRpcApiImpl(app())
private suspend fun app(): KiloBackendAppService {
val app = KiloBackendAppService.create(scope, FakeCliServer(mock), TestLog())
app.connect()
withTimeout(10_000) {
app.appState.first { it is KiloAppState.Ready }
}
return app
}
}
@@ -2,6 +2,9 @@ package ai.kilocode.backend.testing
import java.io.BufferedWriter
import java.io.OutputStreamWriter
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject
import java.net.ServerSocket
import java.net.Socket
import java.net.SocketException
@@ -58,10 +61,21 @@ class MockCliServer : AutoCloseable {
@Volatile var lastWorkspaceConfigPatchPath: String? = null
@Volatile var lastWorkspaceConfigPatchBody: String? = null
@Volatile var lastOrganizationSetBody: String? = null
@Volatile var mcp = "[]"
@Volatile var mcpStatus = 200
@Volatile var mcpActionStatus = 200
@Volatile var agentRemoveStatus = 200
@Volatile var agentBuilderStatus = 200
@Volatile var lastMcpActionPath: String? = null
@Volatile var lastAgentRemoveBody: String? = null
@Volatile var lastAgentBuilderPath: String? = null
@Volatile var lastAgentBuilderBody: String? = null
@Volatile var lastAgentBuilderMethod: String? = null
// Project-scoped REST responses
@Volatile var providers = """{"all":[],"default":{},"connected":[],"failed":[]}"""
@Volatile var providerAuth = "{}"
@Volatile var providersAfterAuthPut: String? = null
@Volatile var agents = "[]"
@Volatile var commands = "[]"
@Volatile var skills = "[]"
@@ -286,6 +300,7 @@ class MockCliServer : AutoCloseable {
bare == "/global/config" && method == "GET" -> respond(output, configStatus, config)
bare == "/global/config" && method == "PATCH" -> {
lastConfigPatchBody = body
config = mergeConfig(config, body)
respond(output, configStatus, config)
}
bare == "/global/dispose" && method == "POST" -> respond(output, disposeStatus, "true")
@@ -293,6 +308,7 @@ class MockCliServer : AutoCloseable {
bare == "/config" && method == "PATCH" -> {
lastWorkspaceConfigPatchPath = path
lastWorkspaceConfigPatchBody = body
workspaceConfig = mergeConfig(workspaceConfig, body)
respond(output, workspaceConfigStatus, workspaceConfig)
}
bare == "/config" -> respond(output, workspaceConfigStatus, workspaceConfig)
@@ -318,6 +334,7 @@ class MockCliServer : AutoCloseable {
}
bare.matches(Regex("/auth/[^/]+")) && method == "PUT" -> {
lastAuthPutBody = body
providersAfterAuthPut?.let { providers = it }
respond(output, authPutStatus, "true")
}
bare == "/kilo/organization" && method == "POST" -> {
@@ -329,8 +346,27 @@ class MockCliServer : AutoCloseable {
bare == "/provider" -> respond(output, providersStatus, providers)
bare == "/provider/auth" -> respond(output, providerAuthStatus, providerAuth)
bare == "/agent" -> respond(output, agentsStatus, agents)
bare == "/agent-builder" || bare.startsWith("/agent-builder/") -> {
lastAgentBuilderPath = path
lastAgentBuilderBody = body
lastAgentBuilderMethod = method
respond(output, agentBuilderStatus, """{"id":"test","scope":"project","path":"/tmp/test.md","markdown":"---\n---\n"}""")
}
bare == "/kilocode/agent/remove" && method == "POST" -> {
lastAgentRemoveBody = body
respond(output, agentRemoveStatus, if (agentRemoveStatus == 200) "true" else """{"error":"Agent not found"}""")
}
bare == "/command" -> respond(output, commandsStatus, commands)
bare == "/skill" -> respond(output, skillsStatus, skills)
bare == "/mcp" -> respond(output, mcpStatus, mcp)
bare.matches(Regex("/mcp/[^/]+/(connect|disconnect)")) && method == "POST" -> {
lastMcpActionPath = path
respond(output, mcpActionStatus, "true")
}
bare.matches(Regex("/mcp/[^/]+/auth/authenticate")) && method == "POST" -> {
lastMcpActionPath = path
respond(output, mcpActionStatus, "true")
}
bare == "/experimental/session" -> {
lastExperimentalSessionPath = path
respond(output, recentSessionsStatus, recentSessions)
@@ -399,6 +435,14 @@ class MockCliServer : AutoCloseable {
writer.flush()
}
private fun mergeConfig(raw: String, patch: String): String {
val base = runCatching { JSON.parseToJsonElement(raw).jsonObject.toMutableMap() }.getOrNull()
?: mutableMapOf()
val next = runCatching { JSON.parseToJsonElement(patch).jsonObject }.getOrNull() ?: return raw
for ((key, value) in next) base[key] = value
return JsonObject(base).toString()
}
private fun handleSse(writer: BufferedWriter, latch: CountDownLatch) {
writer.write("HTTP/1.1 200 OK\r\n")
writer.write("Content-Type: text/event-stream\r\n")
@@ -413,4 +457,8 @@ class MockCliServer : AutoCloseable {
// Block until SSE is closed or server shuts down
latch.await()
}
private companion object {
val JSON = Json { ignoreUnknownKeys = true }
}
}
@@ -5,6 +5,7 @@ import ai.kilocode.client.settings.agents.AgentBehaviorConfigurable
import ai.kilocode.client.settings.models.ModelsConfigurable
import ai.kilocode.client.settings.providers.ProvidersConfigurable
import ai.kilocode.client.settings.profile.UserProfileConfigurable
import ai.kilocode.client.ui.UiStyle
import ai.kilocode.client.ui.layout.Stack
import com.intellij.ide.DataManager
import com.intellij.openapi.options.SearchableConfigurable
@@ -34,10 +35,10 @@ class KiloSettingsConfigurable : SearchableConfigurable {
override fun createComponent(): JComponent {
val panel = Stack.vertical()
panel.border = JBUI.Borders.empty(8, 0, 0, 0)
panel.border = JBUI.Borders.empty(UiStyle.Gap.lg(), 0, 0, 0)
val desc = JBLabel(KiloBundle.message("settings.kilo.description"))
desc.border = JBUI.Borders.emptyBottom(12)
desc.border = JBUI.Borders.emptyBottom(UiStyle.Gap.pad())
panel.next(desc)
val link = ActionLink(KiloBundle.message("settings.profile.displayName")) { e ->
@@ -45,7 +46,7 @@ class KiloSettingsConfigurable : SearchableConfigurable {
val settings = Settings.KEY.getData(DataManager.getInstance().getDataContext(src)) ?: return@ActionLink
open(settings, UserProfileConfigurable.ID)
}
link.border = JBUI.Borders.emptyBottom(4)
link.border = JBUI.Borders.emptyBottom(UiStyle.Gap.sm())
panel.next(link)
val models = ActionLink(KiloBundle.message("settings.models.displayName")) { e ->
@@ -53,7 +54,7 @@ class KiloSettingsConfigurable : SearchableConfigurable {
val settings = Settings.KEY.getData(DataManager.getInstance().getDataContext(src)) ?: return@ActionLink
open(settings, ModelsConfigurable.ID)
}
models.border = JBUI.Borders.emptyBottom(4)
models.border = JBUI.Borders.emptyBottom(UiStyle.Gap.sm())
panel.next(models)
val providers = ActionLink(KiloBundle.message("settings.providers.displayName")) { e ->
@@ -61,7 +62,7 @@ class KiloSettingsConfigurable : SearchableConfigurable {
val settings = Settings.KEY.getData(DataManager.getInstance().getDataContext(src)) ?: return@ActionLink
open(settings, ProvidersConfigurable.ID)
}
providers.border = JBUI.Borders.emptyBottom(4)
providers.border = JBUI.Borders.emptyBottom(UiStyle.Gap.sm())
panel.next(providers)
val behavior = ActionLink(KiloBundle.message("settings.agentBehavior.displayName")) { e ->
@@ -69,7 +70,7 @@ class KiloSettingsConfigurable : SearchableConfigurable {
val settings = Settings.KEY.getData(DataManager.getInstance().getDataContext(src)) ?: return@ActionLink
open(settings, AgentBehaviorConfigurable.ID)
}
behavior.border = JBUI.Borders.emptyBottom(4)
behavior.border = JBUI.Borders.emptyBottom(UiStyle.Gap.sm())
panel.next(behavior)
return panel
@@ -1,6 +1,7 @@
package ai.kilocode.client.settings.agents
import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.ui.UiStyle
import ai.kilocode.client.ui.layout.Stack
import com.intellij.ide.DataManager
import com.intellij.openapi.options.SearchableConfigurable
@@ -17,9 +18,9 @@ class AgentBehaviorConfigurable : SearchableConfigurable {
override fun createComponent(): JComponent {
val panel = Stack.vertical()
panel.border = JBUI.Borders.empty(8, 0, 0, 0)
panel.border = JBUI.Borders.empty(UiStyle.Gap.lg(), 0, 0, 0)
val desc = JBLabel(KiloBundle.message("settings.agentBehavior.description"))
desc.border = JBUI.Borders.emptyBottom(12)
desc.border = JBUI.Borders.emptyBottom(UiStyle.Gap.pad())
panel.next(desc)
listOf(
KiloBundle.message("settings.agentBehavior.agents.displayName") to AgentsConfigurable.ID,
@@ -29,7 +30,7 @@ class AgentBehaviorConfigurable : SearchableConfigurable {
val src = e.source as? JComponent ?: return@ActionLink
val settings = Settings.KEY.getData(DataManager.getInstance().getDataContext(src)) ?: return@ActionLink
settings.find(id)?.let { settings.select(it) }
}.apply { border = JBUI.Borders.emptyBottom(4) })
}.apply { border = JBUI.Borders.emptyBottom(UiStyle.Gap.sm()) })
}
return panel
}
@@ -59,7 +59,7 @@ internal class AgentCreateDialog(private val names: Collection<String>) : Dialog
initValidation()
}
internal fun contentForTest(): JComponent = center ?: error("center panel not built")
internal fun centerComponent(): JComponent = center ?: error("center panel not built")
override fun result(): AgentCreateDto = AgentCreateDto(
name = id.text.trim(),
@@ -89,7 +89,7 @@ internal class AgentEditDialog(
initValidation()
}
internal fun contentForTest(): JComponent = center ?: error("center panel not built")
internal fun centerComponent(): JComponent = center ?: error("center panel not built")
fun result(): AgentEditDraft = agent.copy(
description = text(description.text),
@@ -117,7 +117,7 @@ internal class AgentEditDialog(
row(SettingsStackedRow(
KiloBundle.message("settings.agentBehavior.agents.edit.name"),
value = identity(),
action = exportButton().takeIf { canDelete(agent) },
action = exportButton().takeIf { !agent.native },
))
row(SettingsRow(
KiloBundle.message("settings.agentBehavior.agents.edit.mode"),
@@ -326,7 +326,7 @@ internal class AgentEditDialog(
"agent.json",
)
val wrapper = FileChooserFactory.getInstance()
.createSaveFileDialog(descriptor, contentForTest())
.createSaveFileDialog(descriptor, centerComponent())
.save(null as VirtualFile?, file) ?: return
val json = buildAgentExport(result())
ApplicationManager.getApplication().executeOnPooledThread {
@@ -33,6 +33,7 @@ import com.intellij.openapi.components.service
import com.intellij.openapi.fileChooser.FileChooser
import com.intellij.openapi.fileChooser.FileChooserDescriptor
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.ui.components.JBLabel
import kotlinx.coroutines.CoroutineScope
@@ -41,7 +42,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.nio.charset.StandardCharsets
import javax.swing.JComponent
import javax.swing.JComboBox
private val edt = Dispatchers.EDT + ModalityState.any().asContextElement()
@@ -74,7 +74,7 @@ internal class AgentsSettingsUi(
private var details = emptyList<AgentDetailDto>()
private var models = emptyList<ModelPicker.Item>()
private var names = emptyList<String>()
private lateinit var picker: JComboBox<String>
private lateinit var picker: ComboBox<String>
private var syncing = false
init {
@@ -110,9 +110,9 @@ internal class AgentsSettingsUi(
syncPicker()
}
private fun makePicker(): JComboBox<String> {
private fun makePicker(): ComboBox<String> {
if (::picker.isInitialized) return picker
picker = JComboBox(names.toTypedArray()).apply {
picker = ComboBox(names.toTypedArray()).apply {
selectedItem = draft.defaultAgent.orEmpty()
addActionListener {
if (syncing) return@addActionListener
@@ -16,13 +16,20 @@ import ai.kilocode.rpc.dto.McpConfigDto
import ai.kilocode.rpc.dto.McpServerConfigDto
import ai.kilocode.rpc.dto.McpStatusDto
import com.intellij.icons.AllIcons
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.ui.Messages
import com.intellij.ui.components.JBLabel
import com.intellij.util.ui.UIUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.swing.JComponent
private val edt = Dispatchers.EDT + ModalityState.any().asContextElement()
class McpConfigurable : AgentBehaviorConfigurableBase<JComponent>() {
override fun getId(): String = ID
override fun getDisplayName(): String = KiloBundle.message("settings.agentBehavior.mcp.displayName")
@@ -42,7 +49,6 @@ internal class McpSettingsUi(
) : SettingsListPanel(cs, SettingsListConfig.Equal.copy(description = false)) {
private var dir = dir
@Volatile
private var servers: Map<String, McpServerConfigDto> = emptyMap()
init {
@@ -58,7 +64,7 @@ internal class McpSettingsUi(
override suspend fun fetch(): List<SettingsListItem> {
val behavior = service<KiloAgentBehaviorService>()
val cfg = behavior.mcpConfig(dir)
servers = cfg
withContext(edt) { servers = cfg }
val statuses = if (dir.isBlank()) {
LOG.warn("mcp settings fetch skipped runtime status: missing project directory config=${cfg.size}")
emptyMap()
@@ -80,7 +80,7 @@ internal class McpEditDialog(
init()
}
internal fun contentForTest(): JComponent = center ?: error("center panel not built")
internal fun centerComponent(): JComponent = center ?: error("center panel not built")
override fun result(): McpConfigDto {
if (type == REMOTE) return cfg.copy(url = text(url.text))
@@ -287,12 +287,14 @@ internal class McpEditDialog(
companion object {
fun actionSize(): Dimension = action().preferredSize
private fun action() = SettingsListActionCell(SettingsListCell(
"delete",
KiloBundle.message("common.delete"),
icon = AllIcons.Actions.GC,
iconOnly = true,
))
private fun action() = SettingsListActionCell().apply {
update(SettingsListCell(
"delete",
KiloBundle.message("common.delete"),
icon = AllIcons.Actions.GC,
iconOnly = true,
))
}
}
}
@@ -1,61 +0,0 @@
package ai.kilocode.client.settings.base
import ai.kilocode.client.ui.UiStyle
import ai.kilocode.client.ui.layout.Stack
import ai.kilocode.client.ui.layout.StackAxis
import com.intellij.icons.AllIcons
import com.intellij.openapi.util.text.StringUtil
import com.intellij.ui.components.JBTextField
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.util.ui.JBUI
import java.awt.BorderLayout
import javax.swing.JButton
import javax.swing.JPanel
internal class SettingsListEditor(
private var items: List<String> = emptyList(),
private val onChange: (List<String>) -> Unit,
) : Stack(StackAxis.VERTICAL, UiStyle.Gap.sm()) {
private val field = JBTextField()
private val rows = SettingsRows()
init {
val add = JButton("Add")
add.addActionListener {
val value = field.text.trim()
if (value.isBlank()) return@addActionListener
field.text = ""
update(items + value)
onChange(items)
}
next(JPanel(BorderLayout(UiStyle.Gap.sm(), 0)).apply {
add(field, BorderLayout.CENTER)
add(add, BorderLayout.EAST)
})
next(rows)
sync()
}
@RequiresEdt
fun update(next: List<String>) {
items = next
sync()
}
private fun sync() {
items.forEachIndexed { idx, item ->
val key = idx.toString()
val value = JButton(AllIcons.Actions.Close).apply {
border = JBUI.Borders.empty()
addActionListener {
update(items.filterIndexed { i, _ -> i != idx })
onChange(items)
}
}
if (rows.update(key, StringUtil.shortenTextWithEllipsis(item, 96, 0), value = value) == null) {
rows.row(key, SettingsRow(StringUtil.shortenTextWithEllipsis(item, 96, 0), value = value))
}
}
rows.retain(items.indices.map { it.toString() }.toSet())
}
}
@@ -92,7 +92,8 @@ internal fun settingsListCellBounds(
}
internal fun settingsListCellSize(list: JList<*>, cell: SettingsListCell): Dimension {
val label = SettingsListActionCell(cell).apply {
val label = SettingsListActionCell().apply {
update(cell)
font = list.font
isEnabled = cell.enabled
}
@@ -15,7 +15,6 @@ import com.intellij.ui.components.JBLabel
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import java.awt.BorderLayout
import java.awt.Dimension
import javax.swing.JList
import javax.swing.JPanel
import javax.swing.ListCellRenderer
@@ -103,50 +102,43 @@ internal class SettingsListRenderer(
}
private fun syncBadges(item: SettingsListItem) {
badges.removeAll()
badges.isVisible = item.badges.isNotEmpty()
for (badge in item.badges) {
val items = item.badges
while (badges.componentCount > items.size) badges.remove(badges.componentCount - 1)
while (badges.componentCount < items.size) {
badges.add(JBLabel().apply {
border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap())
icon = FilledBadgeIcon(badge.text, badge.style)
})
}
badges.isVisible = items.isNotEmpty()
for (i in items.indices) {
val badge = items[i]
val label = badges.getComponent(i) as JBLabel
val current = label.icon as? FilledBadgeIcon
if (current?.text != badge.text || current.style != badge.style) {
label.icon = FilledBadgeIcon(badge.text, badge.style)
}
}
}
private fun syncCells(item: SettingsListItem, selected: Boolean, enabled: Boolean) {
cells.removeAll()
val visible = if (enabled) settingsListVisibleCells(item, selected) else emptyList()
while (cells.componentCount > visible.size) cells.remove(cells.componentCount - 1)
while (cells.componentCount < visible.size) cells.add(SettingsListActionCell())
cells.isVisible = visible.isNotEmpty()
cellPane.isVisible = visible.isNotEmpty()
for (cell in visible) {
cells.add(SettingsListActionCell(cell).apply {
isEnabled = cell.enabled
})
for (i in visible.indices) {
(cells.getComponent(i) as SettingsListActionCell).update(visible[i])
}
}
internal fun cellTexts() = cells.components.filterIsInstance<JBLabel>().map { it.text }
internal fun cellIcons() = cells.components.filterIsInstance<JBLabel>().map { it.icon }
internal fun cellLabels() = cells.components.filterIsInstance<JBLabel>()
internal fun badgeTexts() = badges.components.filterIsInstance<JBLabel>().mapNotNull { (it.icon as? FilledBadgeIcon)?.text }
internal fun descriptionText() = desc.text
internal fun iconVisible() = icon.icon != null
internal fun iconSize() = icon.icon?.let { Dimension(it.iconWidth, it.iconHeight) }
}
internal class SettingsListActionCell(cell: SettingsListCell) : JBLabel(cell.label) {
init {
if (cell.iconOnly) text = ""
internal class SettingsListActionCell : JBLabel() {
fun update(cell: SettingsListCell) {
text = if (cell.iconOnly) "" else cell.label
icon = cell.icon
toolTipText = cell.label.takeIf { it.isNotBlank() }
horizontalAlignment = SwingConstants.CENTER
isEnabled = cell.enabled
if (!cell.iconOnly) UiStyle.Components.actionLabel(this, isEnabled)
}
@@ -80,22 +80,6 @@ internal fun providerListRows(state: ProviderSettingsDto, query: String, disable
return rows
}
internal fun providerListIndex(rows: List<ProviderListRow>, key: String?): Int {
if (key == null) return if (rows.isEmpty()) -1 else 0
return rows.indexOfFirst { it.key == key }
}
internal fun providerListIndex(rows: List<ProviderListRow>, index: Int): Int {
if (rows.isEmpty()) return -1
return index.coerceIn(0, rows.lastIndex)
}
internal fun providerListSectionTitle(rows: List<ProviderListRow>, index: Int): String? {
val row = rows.getOrNull(index) ?: return null
val prev = rows.getOrNull(index - 1)
return if (prev?.section != row.section) row.section else null
}
internal fun providerActions(
provider: ProviderSettingsProviderDto,
state: ProviderSettingsDto,
@@ -5,6 +5,7 @@ import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.settings.base.BaseContentPanel
import ai.kilocode.client.settings.base.SettingsPanel
import ai.kilocode.client.settings.base.SettingsListConfig
import ai.kilocode.client.settings.base.SettingsToolbarAction
import ai.kilocode.client.settings.base.SettingsListView
import ai.kilocode.client.settings.auth.DeviceOAuthInfo
import ai.kilocode.client.settings.auth.DeviceOAuthPanel
@@ -27,8 +28,6 @@ import com.intellij.icons.AllIcons
import com.intellij.ide.BrowserUtil
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.CommonShortcuts
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.application.ApplicationManager
@@ -37,7 +36,7 @@ import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.service
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.ui.ValidationInfo
@@ -62,14 +61,12 @@ import java.awt.BorderLayout
import java.awt.event.KeyEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.JComboBox
import javax.swing.JComponent
import javax.swing.DefaultListCellRenderer
import javax.swing.JList
import javax.swing.KeyStroke
import javax.swing.ListSelectionModel
import javax.swing.event.DocumentEvent
import javax.swing.Icon
import javax.swing.Timer
private val edt = Dispatchers.EDT + ModalityState.any().asContextElement()
@@ -86,13 +83,13 @@ internal class ProvidersSettingsUi(
val LOG = KiloLog.create(ProvidersSettingsUi::class.java)
}
private val add = ProviderToolbarAction(
private val add = SettingsToolbarAction(
KiloBundle.message("settings.providers.addCustom"),
KiloBundle.message("settings.providers.addCustom.description"),
AllIcons.General.Add,
{ !busy },
) { custom() }
private val refresh = ProviderToolbarAction(
private val refresh = SettingsToolbarAction(
KiloBundle.message("settings.providers.refresh"),
KiloBundle.message("settings.providers.refresh.description"),
AllIcons.Actions.Refresh,
@@ -490,25 +487,6 @@ internal class ProvidersContent(
}
}
private class ProviderToolbarAction(
text: String,
description: String,
icon: Icon,
private val enabled: () -> Boolean,
private val action: () -> Unit,
) : DumbAwareAction(text, description, icon) {
override fun getActionUpdateThread() = ActionUpdateThread.EDT
override fun actionPerformed(e: AnActionEvent) {
if (!enabled()) return
action()
}
override fun update(e: AnActionEvent) {
e.presentation.isEnabled = enabled()
}
}
private class ApiKeyDialog(title: String, method: ProviderAuthMethodDto?) : DialogWrapper(true) {
private val key = JBPasswordField().apply { columns = 50 }
private val fields = method?.prompts.orEmpty().associateWith { prompt ->
@@ -527,7 +505,7 @@ private class ApiKeyDialog(title: String, method: ProviderAuthMethodDto?) : Dial
@RequiresEdt
fun metadata(): Map<String, String> = fields.mapValues { (_, field) ->
when (field) {
is JComboBox<*> -> (field.selectedItem as? ProviderAuthOptionDto)?.value ?: field.selectedItem?.toString().orEmpty()
is ComboBox<*> -> (field.selectedItem as? ProviderAuthOptionDto)?.value ?: field.selectedItem?.toString().orEmpty()
is JBTextField -> field.text
else -> ""
}
@@ -549,8 +527,8 @@ private class ApiKeyDialog(title: String, method: ProviderAuthMethodDto?) : Dial
return null
}
private fun optionBox(options: List<ProviderAuthOptionDto>): JComboBox<ProviderAuthOptionDto> {
val box = JComboBox(options.toTypedArray())
private fun optionBox(options: List<ProviderAuthOptionDto>): ComboBox<ProviderAuthOptionDto> {
val box = ComboBox(options.toTypedArray())
box.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(list: JList<*>?, value: Any?, index: Int, selected: Boolean, focus: Boolean): java.awt.Component {
val item = value as? ProviderAuthOptionDto
@@ -12,7 +12,7 @@ import javax.swing.Icon
internal class FilledBadgeIcon(
internal val text: String,
private val style: UiStyle.Badge.Style,
internal val style: UiStyle.Badge.Style,
private val font: Font = JBFont.small(),
) : Icon {
override fun getIconWidth(): Int {
@@ -2,6 +2,8 @@ package ai.kilocode.client.settings
import ai.kilocode.client.settings.profile.UserProfileConfigurable
import ai.kilocode.client.settings.models.ModelsConfigurable
import ai.kilocode.client.settings.agents.AgentBehaviorConfigurable
import ai.kilocode.client.settings.providers.ProvidersConfigurable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.options.Configurable
import com.intellij.openapi.options.SearchableConfigurable
@@ -27,6 +29,11 @@ class KiloSettingsConfigurableTest : BasePlatformTestCase() {
assertEquals("ai.kilocode.jetbrains.settings.models", ModelsConfigurable.ID)
}
fun `test child provider and behavior ids match xml registration`() {
assertEquals("ai.kilocode.jetbrains.settings.providers", ProvidersConfigurable.ID)
assertEquals("ai.kilocode.jetbrains.settings.agentBehavior", AgentBehaviorConfigurable.ID)
}
fun `test root implements SearchableConfigurable but not Parent`() {
// Root should be SearchableConfigurable so it can be found by ID,
// but NOT SearchableConfigurable.Parent to avoid duplicating XML-registered child configurables.
@@ -72,12 +79,12 @@ class KiloSettingsConfigurableTest : BasePlatformTestCase() {
}
}
fun `test profile link appears before models link`() {
fun `test createComponent contains settings links in order`() {
val cfg = KiloSettingsConfigurable()
edt {
val panel = cfg.createComponent()
val labels = links(panel as Container).map { it.text }
assertTrue("User Profile should appear before Models", labels.indexOf("User Profile") < labels.indexOf("Models"))
assertEquals(listOf("User Profile", "Models", "Providers", "Agent Behavior"), labels)
}
}
@@ -0,0 +1,55 @@
package ai.kilocode.client.settings.agents
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.options.SearchableConfigurable
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.intellij.ui.components.ActionLink
import java.awt.Container
@Suppress("UnstableApiUsage")
class AgentBehaviorConfigurableTest : BasePlatformTestCase() {
fun `test id matches xml registration`() {
val cfg = AgentBehaviorConfigurable()
assertEquals("ai.kilocode.jetbrains.settings.agentBehavior", cfg.id)
}
fun `test child ids match xml registration`() {
assertEquals("ai.kilocode.jetbrains.settings.agentBehavior.agents", AgentsConfigurable.ID)
assertEquals("ai.kilocode.jetbrains.settings.agentBehavior.mcp", McpConfigurable.ID)
}
fun `test createComponent contains child links in order`() {
val cfg = AgentBehaviorConfigurable()
edt {
val panel = cfg.createComponent()
val labels = links(panel as Container).map { it.text }
assertEquals(listOf("Agents", "MCP Servers"), labels)
}
}
fun `test navigation page is inert`() {
val cfg = AgentBehaviorConfigurable()
assertTrue(cfg is SearchableConfigurable)
assertFalse(cfg.isModified)
cfg.apply()
assertFalse(cfg.isModified)
}
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 links(root: Container): List<ActionLink> = buildList {
for (comp in root.components) {
if (comp is ActionLink) add(comp)
if (comp is Container) addAll(links(comp))
}
}
}
@@ -37,14 +37,14 @@ class AgentCreateDialogTest : BasePlatformTestCase() {
fun `test agent id field defaults to fifty columns`() {
val d = open(emptyList())
assertEquals(50, edt { field<JBTextField>(d.contentForTest(), title("name")).columns })
assertEquals(50, edt { field<JBTextField>(d.centerComponent(), title("name")).columns })
}
fun `test reads form values into dto`() {
val d = open(emptyList())
val result = edt {
val root = d.contentForTest()
val root = d.centerComponent()
field<JBTextField>(root, title("name")).text = "reviewer"
field<EditorTextField>(root, title("prompt")).text = "Review carefully"
field<JBTextArea>(root, title("description")).text = "Reviews code"
@@ -61,7 +61,7 @@ class AgentCreateDialogTest : BasePlatformTestCase() {
val d = open(emptyList())
val result = edt {
val root = d.contentForTest()
val root = d.centerComponent()
field<JBTextField>(root, title("name")).text = " spacer "
field<EditorTextField>(root, title("prompt")).text = " Prompt "
field<JBTextArea>(root, title("description")).text = " "
@@ -61,7 +61,7 @@ class AgentEditDialogTest : BasePlatformTestCase() {
val d = open(agent)
edt {
val root = d.contentForTest()
val root = d.centerComponent()
assertEquals("Review desc", field<JBTextArea>(root, title("description")).text)
assertEquals("Prompt text", field<EditorTextField>(root, title("prompt")).text)
assertEquals("0.4", field<JBTextField>(root, title("temperature")).text)
@@ -81,7 +81,7 @@ class AgentEditDialogTest : BasePlatformTestCase() {
val d = open(draft())
val result = edt {
val root = d.contentForTest()
val root = d.centerComponent()
field<JBTextArea>(root, title("description")).text = "New desc"
field<EditorTextField>(root, title("prompt")).text = "New prompt"
field<JBTextField>(root, title("temperature")).text = "0.2"
@@ -113,7 +113,7 @@ class AgentEditDialogTest : BasePlatformTestCase() {
val d = open(agent)
edt {
val root = d.contentForTest()
val root = d.centerComponent()
assertFalse(field<JBTextArea>(root, title("description")).isEditable)
assertFalse(field<ComboBox<*>>(root, title("mode")).isEnabled)
assertFalse(hasRow(root, title("hidden")))
@@ -131,7 +131,7 @@ class AgentEditDialogTest : BasePlatformTestCase() {
val d = open(draft())
edt {
val root = d.contentForTest()
val root = d.centerComponent()
val button = descendants(rowByTitle(root, title("name"))).filterIsInstance<HoverIcon>().first()
val text = KiloBundle.message("settings.agentBehavior.agents.edit.export")
assertTrue(button.isEnabled)
@@ -145,7 +145,7 @@ class AgentEditDialogTest : BasePlatformTestCase() {
val d = open(draft().copy(native = true))
edt {
val root = d.contentForTest()
val root = d.centerComponent()
assertTrue(descendants(rowByTitle(root, title("name"))).filterIsInstance<HoverIcon>().isEmpty())
true
}
@@ -35,7 +35,7 @@ class McpEditDialogTest : BasePlatformTestCase() {
val d = open(local())
edt {
val root = d.contentForTest()
val root = d.centerComponent()
assertEquals("node", field<JBTextField>(root, title("command")).text)
assertEquals("server.js\n--flag", field<JBTextArea>(root, title("args")).text)
assertTrue(hasRow(root, "TOKEN=x"))
@@ -50,7 +50,7 @@ class McpEditDialogTest : BasePlatformTestCase() {
val d = open(remote())
edt {
val root = d.contentForTest()
val root = d.centerComponent()
assertEquals("https://mcp.example.test", field<JBTextField>(root, title("url")).text)
assertTrue(hasRow(root, title("url")))
assertFalse(hasRow(root, title("command")))
@@ -63,7 +63,7 @@ class McpEditDialogTest : BasePlatformTestCase() {
val d = open(local())
val result = edt {
val root = d.contentForTest()
val root = d.centerComponent()
field<JBTextField>(root, title("command")).text = "bun"
field<JBTextArea>(root, title("args")).text = "mcp.ts\n\n--watch"
d.result()
@@ -80,7 +80,7 @@ class McpEditDialogTest : BasePlatformTestCase() {
val d = open(remote())
val result = edt {
val root = d.contentForTest()
val root = d.centerComponent()
field<JBTextField>(root, title("url")).text = "https://new.example.test/mcp"
d.result()
}
@@ -95,7 +95,7 @@ class McpEditDialogTest : BasePlatformTestCase() {
val d = open(local())
val result = edt {
val root = d.contentForTest()
val root = d.centerComponent()
val fields = descendants(root).filterIsInstance<JBTextField>()
fields[1].text = "NEXT"
fields[2].text = "value"
@@ -159,7 +159,7 @@ class SettingsListViewTest : BasePlatformTestCase() {
val title = components(renderer).filterIsInstance<SimpleColoredComponent>().single()
val action = components(renderer).filterIsInstance<JBLabel>().single { it.text == "Edit" }
assertEquals("", renderer.descriptionText())
assertTrue(components(renderer).filterIsInstance<JBLabel>().none { it.text == "Description" && it.isVisible })
assertNull(view.list.getToolTipText(event(view.list, Point(bounds.x + 4, bounds.y + 4))))
assertTrue(kotlin.math.abs(centerY(renderer, title) - centerY(renderer, action)) <= 1)
}
@@ -3,8 +3,10 @@ package ai.kilocode.client.settings.providers
import ai.kilocode.client.app.KiloProviderService
import ai.kilocode.client.settings.base.SettingsListItem
import ai.kilocode.client.settings.base.SettingsListRenderer
import ai.kilocode.client.settings.base.SettingsListActionCell
import ai.kilocode.client.settings.base.settingsListCellAt
import ai.kilocode.client.settings.base.settingsListCellBounds
import ai.kilocode.client.settings.base.settingsListSectionTitle
import ai.kilocode.client.settings.base.settingsListVisibleCells
import ai.kilocode.client.testing.FakeProviderRpcApi
import ai.kilocode.client.ui.UiStyle
@@ -218,7 +220,7 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() {
)
assertEquals(listOf("kilo", "anthropic", "deepseek", "openai", "google", "openrouter", "vercel"), rows.map { it.key })
assertEquals("Popular providers", providerListSectionTitle(rows, 0))
assertEquals("Popular providers", settingsListSectionTitle(rows, 0))
}
fun `test popular rows use fallback order without metadata`() {
@@ -234,8 +236,8 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() {
)
assertEquals(listOf("anthropic", "openai", "unknown"), rows.map { it.key })
assertEquals("Popular providers", providerListSectionTitle(rows, 0))
assertEquals("All providers", providerListSectionTitle(rows, 2))
assertEquals("Popular providers", settingsListSectionTitle(rows, 0))
assertEquals("All providers", settingsListSectionTitle(rows, 2))
}
fun `test connected providers appear first and are not duplicated in popular section`() {
@@ -248,8 +250,8 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() {
)
assertEquals(listOf("anthropic", "openai"), rows.map { it.key })
assertEquals("Connected providers", providerListSectionTitle(rows, 0))
assertEquals("Popular providers", providerListSectionTitle(rows, 1))
assertEquals("Connected providers", settingsListSectionTitle(rows, 0))
assertEquals("Popular providers", settingsListSectionTitle(rows, 1))
assertEquals(listOf(ProviderListAction.DISCONNECT), rows[0].actions)
assertTrue(rows[0].connected)
}
@@ -268,9 +270,9 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() {
)
assertEquals(listOf("local-openai", "anthropic", "available-custom"), rows.map { it.key })
assertEquals("Connected providers", providerListSectionTitle(rows, 0))
assertEquals("Popular providers", providerListSectionTitle(rows, 1))
assertEquals("All providers", providerListSectionTitle(rows, 2))
assertEquals("Connected providers", settingsListSectionTitle(rows, 0))
assertEquals("Popular providers", settingsListSectionTitle(rows, 1))
assertEquals("All providers", settingsListSectionTitle(rows, 2))
assertEquals(listOf(ProviderListAction.DISCONNECT), rows[0].actions)
}
@@ -295,7 +297,7 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() {
)
assertEquals(listOf("kilo"), rows.map { it.key })
assertEquals("Connected providers", providerListSectionTitle(rows, 0))
assertEquals("Connected providers", settingsListSectionTitle(rows, 0))
assertTrue(rows.single().actions.isEmpty())
}
@@ -309,7 +311,7 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() {
)
assertEquals(listOf("openai", "anthropic"), rows.map { it.key })
assertEquals("All providers", providerListSectionTitle(rows, 1))
assertEquals("All providers", settingsListSectionTitle(rows, 1))
assertEquals(listOf(ProviderListAction.ENABLE), rows[1].actions)
}
@@ -326,7 +328,7 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() {
)
assertEquals(listOf("openai", "alpha", "zeta"), rows.map { it.key })
assertEquals("All providers", providerListSectionTitle(rows, 1))
assertEquals("All providers", settingsListSectionTitle(rows, 1))
}
fun `test filtering by provider name updates rows and sections`() {
@@ -346,7 +348,7 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() {
val rows = rows(content)
assertEquals(listOf("openai"), rows.map { it.key })
assertEquals("Popular providers", providerListSectionTitle(rows, 0))
assertEquals("Popular providers", settingsListSectionTitle(rows, 0))
}
}
@@ -405,7 +407,7 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() {
render(renderer, list, row, selected = true)
assertEquals(listOf("OAuth", "Connect"), renderer.cellTexts())
assertEquals(listOf("OAuth", "Connect"), actionTexts(renderer))
}
}
@@ -435,7 +437,7 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() {
render(renderer, list, row, selected = false)
assertTrue(renderer.cellTexts().isEmpty())
assertTrue(actionTexts(renderer).isEmpty())
}
}
@@ -451,7 +453,7 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() {
assertTrue(visibleActions(row, selected = true).isEmpty())
assertTrue(actionBounds(list, bounds, row, selected = true).isEmpty())
assertNull(actionAt(list, bounds, Point(300, 24), row, selected = true))
assertTrue(renderer.cellTexts().isEmpty())
assertTrue(actionTexts(renderer).isEmpty())
}
}
@@ -488,9 +490,8 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() {
render(renderer, list, row, selected = true)
assertTrue(renderer.iconVisible())
assertEquals(Dimension(JBUI.scale(20), JBUI.scale(20)), renderer.iconSize())
assertEquals("GPT and Codex models with API key or ChatGPT login", renderer.descriptionText())
assertEquals(Dimension(JBUI.scale(20), JBUI.scale(20)), iconSizes(renderer).single())
assertEquals("GPT and Codex models with API key or ChatGPT login", descriptions(renderer).single())
assertTrue(renderer.preferredSize.height > JBUI.scale(44))
}
}
@@ -512,7 +513,7 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() {
render(renderer, list, row, selected = true)
assertEquals("Build with OpenAI models", renderer.descriptionText())
assertEquals("Build with OpenAI models", descriptions(renderer).single())
}
}
@@ -524,7 +525,7 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() {
render(renderer, list, row, selected = true)
assertEquals("", renderer.descriptionText())
assertTrue(descriptions(renderer).isEmpty())
}
}
@@ -883,6 +884,22 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() {
renderer.getListCellRendererComponent(list as JList<out SettingsListItem>, row, 0, selected, false)
}
private fun actionTexts(renderer: SettingsListRenderer): List<String> = components(renderer)
.filterIsInstance<SettingsListActionCell>()
.filter { it.isVisible }
.mapNotNull { it.text.takeIf(String::isNotBlank) }
private fun descriptions(renderer: SettingsListRenderer): List<String> = components(renderer)
.filterIsInstance<JBLabel>()
.filter { it.isVisible && it !is SettingsListActionCell }
.mapNotNull { it.text.takeIf(String::isNotBlank) }
private fun iconSizes(renderer: SettingsListRenderer): List<Dimension> = components(renderer)
.filterIsInstance<JBLabel>()
.mapNotNull { it.icon }
.filter { it.iconWidth == JBUI.scale(20) && it.iconHeight == JBUI.scale(20) }
.map { Dimension(it.iconWidth, it.iconHeight) }
private fun actionAt(list: JBList<ProviderListRow>, bounds: Rectangle, point: Point, row: ProviderListRow, selected: Boolean): ProviderListAction? {
val id = settingsListCellAt(list, bounds, point, row, selected) ?: return null
return ProviderListAction.entries.firstOrNull { it.name == id }
-20
View File
@@ -1,20 +0,0 @@
declare module "@lydell/node-pty" {
export interface IPty {
pid: number
onData(listener: (data: string) => void): { dispose(): void }
onExit(listener: (event: { exitCode: number; signal?: number }) => void): { dispose(): void }
write(data: string): void
resize(cols: number, rows: number): void
kill(signal?: string): void
}
export interface IPtyForkOptions {
name?: string
cols?: number
rows?: number
cwd?: string
env?: Record<string, string | undefined>
}
export function spawn(file: string, args: string[], opts: IPtyForkOptions): IPty
}
File diff suppressed because one or more lines are too long
@@ -64,19 +64,13 @@ function run(query: string, signal?: AbortSignal) {
})
}
function sessions() {
return Effect.runPromise(Effect.gen(function* () {
return yield* Session.Service
}).pipe(Effect.provide(Session.defaultLayer)))
}
describe("RecallSearch", () => {
test("searches titles and terms distributed across transcript messages", async () => {
await using tmp = await tmpdir({ git: true })
await provideTestInstance({
directory: tmp.path,
fn: async () => {
const service = await sessions()
const service = await Effect.runPromise(Session.Service.pipe(Effect.provide(Session.defaultLayer)))
const session = await Effect.runPromise(service.create({ title: "Quartz migration" }))
add(session.id, "user", { type: "text", text: "Investigate the zephyr request path" })
add(session.id, "assistant", { type: "text", text: "The cobalt adapter needs a bounded scan" })
@@ -101,7 +95,7 @@ describe("RecallSearch", () => {
await provideTestInstance({
directory: tmp.path,
fn: async () => {
const service = await sessions()
const service = await Effect.runPromise(Session.Service.pipe(Effect.provide(Session.defaultLayer)))
const historical = await Effect.runPromise(service.create({ title: "Historical" }))
const active = await Effect.runPromise(service.create({ title: "exclusive-recall-needle" }))
add(historical.id, "user", { type: "text", text: "exclusive-recall-needle" })
@@ -130,7 +124,7 @@ describe("RecallSearch", () => {
await provideTestInstance({
directory: tmp.path,
fn: async () => {
const service = await sessions()
const service = await Effect.runPromise(Session.Service.pipe(Effect.provide(Session.defaultLayer)))
const session = await Effect.runPromise(service.create({ title: "Queued turn" }))
const previous = add(session.id, "user", { type: "text", text: "previous request" })
const active = add(session.id, "user", { type: "text", text: "queued prompt current-turn-needle" })
@@ -179,7 +173,7 @@ describe("RecallSearch", () => {
await provideTestInstance({
directory: tmp.path,
fn: async () => {
const service = await sessions()
const service = await Effect.runPromise(Session.Service.pipe(Effect.provide(Session.defaultLayer)))
const session = await Effect.runPromise(service.create({ title: "Search policy" }))
add(session.id, "user", {
type: "file",
@@ -250,7 +244,7 @@ describe("RecallSearch", () => {
await provideTestInstance({
directory: tmp.path,
fn: async () => {
const service = await sessions()
const service = await Effect.runPromise(Session.Service.pipe(Effect.provide(Session.defaultLayer)))
const parent = await Effect.runPromise(service.create({ title: "Parent" }))
const child = await Effect.runPromise(service.create({ title: "Child", parentID: parent.id }))
await Effect.runPromise(service.setArchived({ sessionID: child.id, time: Date.now() }))
@@ -287,7 +281,7 @@ describe("RecallSearch", () => {
await provideTestInstance({
directory: tmp.path,
fn: async () => {
const service = await sessions()
const service = await Effect.runPromise(Session.Service.pipe(Effect.provide(Session.defaultLayer)))
const session = await Effect.runPromise(service.create({ title: "Large session" }))
add(session.id, "user", { type: "text", text: "job_id reached 100%" })
add(session.id, "user", { type: "text", text: `${"x".repeat(1_000)} Compatibility FOO marker` })
@@ -121,6 +121,33 @@ export const kiloScenarios: Scenario[] = [
.mutating()
.at((ctx) => ({ path: "/config/rules", headers: ctx.headers(), body: { content: "Use small changes." } }))
.json(200, object),
http.protected
.put("/auth/{providerID}", "auth.set")
.mutating()
.at((ctx) => ({
path: route("/auth/{providerID}", { providerID: "openai" }),
headers: ctx.headers(),
body: { type: "api", key: "sk-httpapi-test" },
}))
.json(200, (body) => check(body === true, "provider auth set should return true")),
http.protected
.post("/mcp", "mcp.add")
.mutating()
.at((ctx) => ({
path: "/mcp",
headers: ctx.headers(),
body: { name: "httpapi-mcp", config: { type: "remote", url: "https://mcp.example.test" } },
}))
.json(200, object),
http.protected
.post("/mcp", "mcp.add")
.mutating()
.at((ctx) => ({
path: "/mcp",
headers: ctx.headers(),
body: { name: "httpapi-mcp", config: { type: "remote", url: "https://mcp-edit.example.test" } },
}))
.json(200, object),
http.protected.get("/config/sources", "config.sources").json(200, object),
http.protected.get("/tui/config", "tui.config.get").json(200, object),
http.protected.get("/tui/keybinds", "tui.keybind.list").json(200, object),
+1 -5
View File
@@ -11,11 +11,7 @@
"paths": {
"@/*": ["./src/*"],
"@tui/*": ["./src/cli/cmd/tui/*"],
"@test/*": ["./test/*"],
"@opencode-ai/http-recorder": ["../http-recorder/src/index.ts"],
"@opencode-ai/llm": ["../llm/src/index.ts"],
"@opencode-ai/llm/route": ["../llm/src/route/index.ts"],
"@opencode-ai/llm/providers": ["../llm/src/providers/index.ts"]
"@test/*": ["./test/*"]
}
}
}