mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
fix(jetbrains): remediate settings apply lifecycle
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Fix opening JetBrains Agent Behavior settings in frontend runs.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Support editing agent behavior settings from the JetBrains Agents settings page.
|
||||
@@ -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.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Fix MCP server environment variable edits in the JetBrains settings dialog.
|
||||
@@ -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.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": minor
|
||||
---
|
||||
|
||||
Support exporting custom agents as `.agent.json` definitions from the agent edit dialog.
|
||||
@@ -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 1317–1469 (`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 26–55):
|
||||
|
||||
```
|
||||
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 9–13) 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 626–649) 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 1657–1692) 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 176–203) 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` 1317–1437), 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 42–64).
|
||||
- `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.
|
||||
@@ -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 116–131, 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 1–3)
|
||||
|
||||
- **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.
|
||||
@@ -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
|
||||
|
||||
+67
@@ -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.
|
||||
|
||||
+15
-74
@@ -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? {
|
||||
|
||||
+31
@@ -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"}}}"""
|
||||
|
||||
+27
@@ -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 = """{
|
||||
|
||||
+142
@@ -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
|
||||
}
|
||||
}
|
||||
+48
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
+7
-6
@@ -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
|
||||
|
||||
+4
-3
@@ -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
|
||||
}
|
||||
|
||||
+1
-1
@@ -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(),
|
||||
|
||||
+3
-3
@@ -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 {
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
+8
-2
@@ -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()
|
||||
|
||||
+9
-7
@@ -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,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-61
@@ -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())
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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
|
||||
}
|
||||
|
||||
+20
-28
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
-16
@@ -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,
|
||||
|
||||
+7
-29
@@ -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
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+9
-2
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+55
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -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 = " "
|
||||
|
||||
+5
-5
@@ -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
|
||||
}
|
||||
|
||||
+5
-5
@@ -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"
|
||||
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
|
||||
+37
-20
@@ -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 }
|
||||
|
||||
Vendored
-20
@@ -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),
|
||||
|
||||
@@ -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/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user