fix(jetbrains): clean up workspace path followups

This commit is contained in:
kirillk
2026-06-19 16:28:19 -04:00
parent c452ce866d
commit 4d4cfbfcd3
19 changed files with 19 additions and 2648 deletions
-172
View File
@@ -1,172 +0,0 @@
# Plan: Address `basalt-dirt` review findings (JetBrains slash/mention completion)
Companion to `.kilo/plans/basalt-dirt-review.md`. All work is in `packages/kilo-jetbrains/`
(Kilo-owned — no `kilocode_change` markers, no shared opencode edits).
Decision captured: **remove `@terminal` until implemented** (keep `@git-changes`, which is fully wired).
Execution order below is also the recommended priority. Each task lists files + intent; keep edits
minimal and follow the JetBrains `AGENTS.md` (single-word names, `@RequiresEdt` where applicable,
no test-only production methods, prefer pure helpers for testability).
---
## Task 1 — Remove the half-wired `@terminal` mention
Goal: no dead UX. `@terminal` must not be suggested, highlighted, or leave stub RPC plumbing.
**Frontend**
- `session/ui/prompt/KiloPromptCompletionProvider.kt`
- In `mention()`: delete the `if ("terminal".startsWith(prefix …) && search.terminal)` block.
- In `highlights()`: change the special set from `setOf("terminal", "git-changes")` to
`setOf("git-changes")`.
- `session/SessionUi.kt`
- In `mentionParts()`: delete the `@terminal` block (and its `runBlocking { workspaces.terminalOutput(...) }`).
- `app/KiloWorkspaceService.kt`: delete `terminalOutput(...)`.
- `resources/messages/KiloBundle.properties`: delete `prompt.mention.terminal`.
**Shared**
- `rpc/KiloWorkspaceRpcApi.kt`: delete `terminalOutput(directory)`.
- `rpc/dto/WorkspaceFileDto.kt`: remove the unused `terminal: Boolean = false` field from `FileSearchResultDto`.
**Backend**
- `rpc/KiloWorkspaceRpcApiImpl.kt`: delete the `terminalOutput()` override. `searchFiles()` already
returns no `terminal` flag — confirm it still compiles after the DTO field removal.
**Tests / fakes to update (compile breakers)**
- `testing/FakeWorkspaceRpcApi.kt`: remove `var terminalOutput` + the `terminalOutput()` override.
- `KiloPromptCompletionProviderTest.kt`:
- `test mention completion includes matching special items`: drop `terminal = true` and the
terminal assertion; keep `git-changes`.
- `test highlights special mentions without tracked paths`: assert only the `@git-changes`
highlight (recompute offsets for a text containing just `@git-changes`).
- `PromptPanelTest.kt`: in the three highlight tests, replace `@terminal` usages with `@git-changes`
(or a tracked file mention) so the highlight/clear/bounded assertions still exercise real spans.
---
## Task 2 — Make the completion provider the single owner of command knowledge (de-dup)
Goal: eliminate the triplicated client-command-name list and the duplicated `/name` parsing.
- `session/ui/prompt/KiloPromptCompletionProvider.kt`
- Add `fun serverCommand(text: String): Pair<String, String>?` that reuses `clientNames()` and
`workspace.state.value.commands` (same rules as today: leading `/`, non-blank name, not a client
name, must match a known server command; returns `name to args`). This centralizes the parsing
already echoed in `highlights()`.
- `session/SessionUi.kt`
- Promote the completion provider to a field (e.g. `private lateinit var completion` assigned in the
existing init block, or store it) so `sendPrompt()` can reach it.
- Replace `SessionUi.serverCommand(text)` with a call to `completion.serverCommand(text)`.
- **Delete `SessionUi.clientCommandNames()`** entirely.
Result: client-action names exist once (in `slashActions()` → provider `actions` → `clientNames()`),
and `/name` routing logic lives once in the provider.
---
## Task 3 — Extract pure mention-assembly helper + fix frontend `runBlocking`
Goal: testable submit-side assembly and AGENTS-compliant blocking.
- New pure helper (top-level functions or a small object in `session/ui/prompt/`, e.g.
`PromptMentionParts.kt`):
- `fun mentionFileParts(text: String, paths: Set<String>, directory: String): List<PromptPartDto>`
— the current tracked-path → `file` part logic (path resolve, `toUri()`, `source(...)`), pure.
- `fun gitChangesPart(text: String, diff: String?): PromptPartDto?` — builds the `data:` URL part
when `@git-changes` is present and `diff` is non-blank (current `dataPart`/`source` logic), pure.
- Move `dataPart()` and `source()` here (or keep private and expose only the two functions).
- `session/SessionUi.kt` `mentionParts()` becomes a thin wrapper:
- `mentionFileParts(text, paths, workspace.directory)` +
- `gitChangesPart(text, runBlockingCancellable { workspaces.gitChanges(workspace.directory) })`.
- **Replace `runBlocking` with `com.intellij.openapi.progress.runBlockingCancellable`** (still on the
pooled thread from `PromptPanel.submit`; cancellation/progress aware). Remove the `kotlinx.coroutines.runBlocking` import.
- While here, tighten the `@git-changes` match to require a word boundary (end-of-token), so
`@git-changes-foo` does not match. Apply the same boundary rule already used in `highlights()`.
---
## Task 4 — Tests for `SessionController.command()`
Goal: cover the new ~50-line controller method (mirrors `prompt()`).
- `testing/FakeSessionRpcApi.kt`: add `var commandThrows: Exception? = null`; in `command(...)`
throw it before recording, so error paths are testable (mirrors `enhanceThrows`). The existing
`commands` list is the assertion target.
- New `controller/CommandLifecycleTest.kt` (extends `SessionControllerTestBase`), modeled on
`PromptLifecycleTest`:
- **New session**: `controller()` + ready app/workspace with a server command named e.g. `deploy`;
`edt { m.command("deploy", "prod") }`; `flush()`; assert one `rpc.commands` entry with
`command="deploy"`, `arguments="prod"`; assert a session was created and events subscribed.
- **Existing session**: from `prompted()`, run `m.command(...)`; assert it reuses the session id and
does not create a new one.
- **Telemetry**: assert `appRpc.telemetry` has `"Conversation Send Clicked"` with `source="command"`
and `"Conversation Message"` with `source="command"`.
- **Error path**: set `rpc.commandThrows`; assert `m.model.state is SessionState.Error` and a
`"Session Error"` telemetry event with `context="command"`.
---
## Task 5 — Tests for submit-side assembly (uses the Task 3 helper)
Goal: cover routing + part building without test-only production methods.
- New `session/ui/prompt/PromptMentionPartsTest.kt` (plain unit test; `BasePlatformTestCase` only if
needed for `PromptPartDto`):
- `mentionFileParts`: tracked path present in text → one `file` part with absolute `file://` url,
`filename`, and `source.path`/`source.text` offsets; untracked path → no part; absolute path in
`paths` kept absolute.
- `gitChangesPart`: `@git-changes` present + non-blank diff → one `data:text/plain` part with
URL-encoded content (`+` → `%20`) and `source.uri="git-changes"`; null/blank diff → no part;
`@git-changes-foo` (no boundary) → no part.
- `serverCommand` routing: add a case to `KiloPromptCompletionProviderTest`:
- With a workspace command `deploy` and client action `new`: `serverCommand("/deploy x")` →
`"deploy" to "x"`; `serverCommand("/new")` → null (client action); `serverCommand("hi /deploy")`
→ null (not leading); `serverCommand("/unknown")` → null.
---
## Task 6 — Add the missing feature changeset
- New `.changeset/jetbrains-prompt-completion.md`, bump `minor`:
- `"@kilocode/kilo-jetbrains": minor`
- User-facing summary, e.g.: "Add `/` slash commands and `@` file/git-changes mentions to the
JetBrains chat prompt with native completion." Imperative, no implementation detail.
- Leave the two existing `patch` changesets (`jetbrains-file-mention-parity`,
`jetbrains-stable-mention-completion`) as-is — they describe follow-up fixes.
---
## Task 7 — Minor cleanups
- `session/SessionUi.kt`: add `import ai.kilocode.client.plugin.KiloBundle` and replace the 8
fully-qualified `ai.kilocode.client.plugin.KiloBundle.message(...)` calls in `slashActions()` with
`KiloBundle.message(...)`.
- `session/ui/prompt/PromptPanel.kt` `showCompletion()`: add a short comment noting it relies on
IntelliJ impl/internal completion APIs (`CodeCompletionHandlerBase`, `LookupImpl`,
`LookupPresentation`, `LookupPositionStrategy`) and may need revisiting on platform upgrades.
(Keep `@Suppress("UnstableApiUsage")` as-is.)
---
## Out of scope (documented limitation, no change)
- Manually typed `@path` mentions (no popup selection) still won't attach a file part — only
popup-inserted paths are tracked. This matches the original design decision; revisit only if
parity with VS Code's text-scan approach is requested.
---
## Verification
From `packages/kilo-jetbrains/` (requires Java 21 — check `java -version`; install via
`sdk install java 21-tem && sdk use java 21-tem` if missing):
- `./gradlew typecheck` — compiles all Kotlin incl. terminal removal across shared/backend/frontend.
- `./gradlew test` (or targeted: `./gradlew test --tests '*KiloPromptCompletionProviderTest*'`
`--tests '*CommandLifecycleTest*'` `--tests '*PromptMentionPartsTest*'` `--tests '*PromptPanelTest*'`).
- Sanity: `bunx changeset status` (or confirm the new changeset parses) from repo root.
Done when: `@terminal` is fully gone, `clientCommandNames()` is deleted, `command()` and
mention-assembly have tests, frontend uses `runBlockingCancellable`, and the `minor` changeset exists,
with typecheck + tests green.
@@ -1,222 +0,0 @@
# Implementation plan — remove internal IntelliJ APIs from the JetBrains prompt completion
Superseded note: Task 1 was intentionally not followed; `CodeCompletionHandlerBase` is retained for split-mode completion behavior. See `basalt-dirt-pr-review-v2.md` and `basalt-dirt-followup-fixes.md` for the current plan.
Audience: an executing model that should follow these steps literally. Read-only research is already
done; the exact edits, rationale, and verification are below. Do **not** expand scope beyond the three
tasks. All paths are under `packages/kilo-jetbrains/`.
## Goal
Eliminate genuinely implementation-only IntelliJ APIs from the prompt slash/mention completion feature,
and bring the remaining unstable-but-public usage into AGENTS compliance (warn + isolate + degrade
gracefully). Keep behavior and all existing tests green.
## Background (verified against `$INTELLIJ_REPO`, do not re-research)
- `CodeCompletionHandlerBase` — `lang-impl`, implementation. Public replacement: perform the standard
`IdeActions.ACTION_CODE_COMPLETION` action against the editor (this is what `myFixture.completeBasic()`
does under the hood; runs synchronously in unit-test mode).
- `com.intellij.codeInsight.lookup.impl.LookupImpl` — `impl` package. Public super-interface
`com.intellij.codeInsight.lookup.LookupEx` (`analysis-api`) declares `getEditor()`,
`getPresentation()`, and `setPresentation(LookupPresentation)`.
- `LookupPresentation` / `LookupPositionStrategy` — public but `@ApiStatus.Experimental`. **Keep** (no
non-experimental way to force the lookup above the caret); just add a one-line warning comment.
- Backend `GotoFileModel`, `ChooseByNameViewModel`, `ChooseByNameInScopeItemProvider`,
`ChooseByNameWeightedItemProvider.filterElementsWithWeights`, `ChooseByNamePopup.getTransformedPattern`,
`FindSymbolParameters.wrap` — all plain `public` members in `lang-impl`. **None are `@Internal` or
`@Experimental`.** This is the proven Go-to-File / Search Everywhere engine. **Keep it**, do not
reimplement; harden with `@Suppress` + comment + a `LinkageError` fallback.
## Scope
- In scope: `frontend/.../session/ui/prompt/PromptPanel.kt`,
`backend/.../rpc/KiloWorkspaceRpcApiImpl.kt`.
- Out of scope (do **not** touch in this task): the larger refactor that deletes `triggerCompletion`/
the custom completion-shortcut action (would require rewriting `PromptPanelTest`), the model-package
moves, the `Dispatchers.IO`/cache items, and any test files. No test edits are required by this plan.
---
## Task 1 — Frontend: replace `CodeCompletionHandlerBase` with the public completion action
File: `frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt`
The custom action `COMPLETION_ACTION_TEXT = "Kilo Prompt Completion"`, `triggerCompletion`, and the
shortcut machinery all stay (tests depend on them). Only the body of `showCompletion` changes.
1.1 Rewrite `showCompletion` (currently ~lines 550-554):
```kotlin
private fun showCompletion(ed: com.intellij.openapi.editor.Editor) {
// Uses IntelliJ impl/internal completion APIs; revisit on platform upgrades.
CodeCompletionHandlerBase.createHandler(CompletionType.BASIC, true, false, true)
.invokeCompletion(project, ed, 1)
}
```
to:
```kotlin
private fun showCompletion(ed: com.intellij.openapi.editor.Editor) {
// Trigger the standard public code-completion action against this editor instead of the
// impl-only CodeCompletionHandlerBase. Runs synchronously in unit-test mode.
val action = ActionManager.getInstance().getAction(IdeActions.ACTION_CODE_COMPLETION) ?: return
val ctx = DataContext { id ->
when (id) {
CommonDataKeys.EDITOR.name -> ed
CommonDataKeys.CARET.name -> ed.caretModel.currentCaret
CommonDataKeys.PROJECT.name -> project
else -> null
}
}
val event = AnActionEvent.createEvent(action, ctx, null, ActionPlaces.UNKNOWN, ActionUiKind.NONE, null)
ActionUtil.updateAction(action, event)
ActionUtil.performAction(action, event)
}
```
Rationale for the explicit `DataContext` lambda (not `DataManager.getDataContext`): it deterministically
supplies EDITOR/CARET/PROJECT and mirrors exactly the context the existing `PromptPanelTest` helpers use,
so the headless completion tests stay reliable.
1.2 Imports — remove the two now-unused completion imports, add the two data-context imports:
- Remove `import com.intellij.codeInsight.completion.CodeCompletionHandlerBase`
- Remove `import com.intellij.codeInsight.completion.CompletionType`
- Add `import com.intellij.openapi.actionSystem.CommonDataKeys`
- Add `import com.intellij.openapi.actionSystem.DataContext`
Keep all other imports (`ActionManager`, `ActionUtil`, `AnActionEvent`, `ActionPlaces`, `ActionUiKind`,
`IdeActions`, `DataManager`, `DumbAwareAction`, `AnAction` are still used elsewhere in the file).
---
## Task 2 — Frontend: replace `LookupImpl` with public `LookupEx`
File: `frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt`
2.1 In `bindLookup` (currently ~lines 715-727), change the cast and add the experimental-API note:
```kotlin
connection.subscribe(LookupManagerListener.TOPIC, LookupManagerListener { _, next ->
val lookup = next as? LookupImpl ?: return@LookupManagerListener
if (lookup.editor !== editor.getEditor(false)) return@LookupManagerListener
lookup.presentation = LookupPresentation.Builder(lookup.presentation)
.withPositionStrategy(LookupPositionStrategy.ONLY_ABOVE)
.build()
})
```
to:
```kotlin
connection.subscribe(LookupManagerListener.TOPIC, LookupManagerListener { _, next ->
// LookupPresentation/LookupPositionStrategy are @ApiStatus.Experimental (no stable API
// forces the lookup above the caret). LookupEx is the public lookup interface.
val lookup = next as? LookupEx ?: return@LookupManagerListener
if (lookup.editor !== editor.getEditor(false)) return@LookupManagerListener
lookup.presentation = LookupPresentation.Builder(lookup.presentation)
.withPositionStrategy(LookupPositionStrategy.ONLY_ABOVE)
.build()
})
```
2.2 Imports:
- Remove `import com.intellij.codeInsight.lookup.impl.LookupImpl`
- Add `import com.intellij.codeInsight.lookup.LookupEx`
- Keep `LookupManagerListener`, `LookupPresentation`, `LookupPositionStrategy` imports.
Note: `PromptPanelTest` still references `LookupImpl` on the test side (`acceptLookup`, the
positioned-above-caret assertion). That is fine — tests may inspect the impl; do **not** change them.
---
## Task 3 — Backend: keep the proven Go-to-File engine, harden it
File: `backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceRpcApiImpl.kt`
3.1 Annotate the `search(...)` adapter (currently starts ~line 316) and document the unstable-API reliance:
```kotlin
private fun search(project: Project, base: Path, query: String, limit: Int): List<WorkspaceFileDto> {
```
to:
```kotlin
// Uses the IDE Go-to-File engine (com.intellij.ide.util.gotoByName.*). These are public but
// unstable lang-impl classes (not @ApiStatus.Internal) — the same engine behind Search Everywhere,
// chosen for proven large-repo performance. searchFiles() degrades gracefully on LinkageError.
@Suppress("UnstableApiUsage")
private fun search(project: Project, base: Path, query: String, limit: Int): List<WorkspaceFileDto> {
```
(If the DevKit inspection still flags the import statements at file scope after this, fall back to a
file-level `@file:Suppress("UnstableApiUsage")` at the top of the file. Prefer the function-level form.)
3.2 Add a `LinkageError` fallback in `searchFiles` (currently ~lines 194-205) so an API drift in a future
platform degrades to "no suggestions" instead of crashing the prompt. Change:
```kotlin
return try {
val files = readAction { search(project, base, query, limit.coerceIn(1, 200)) }
FileSearchResultDto(files = files, git = git)
} catch (e: IndexNotReadyException) {
FileSearchResultDto(indexing = true, git = git)
}
```
to:
```kotlin
return try {
val files = readAction { search(project, base, query, limit.coerceIn(1, 200)) }
FileSearchResultDto(files = files, git = git)
} catch (e: IndexNotReadyException) {
FileSearchResultDto(indexing = true, git = git)
} catch (e: LinkageError) {
LOG.warn("file search API unavailable; returning no suggestions", e)
FileSearchResultDto(git = git)
}
```
No new imports needed (`LinkageError` is `java.lang`; `LOG` already exists in the class).
---
## Verification (run from `packages/kilo-jetbrains/`, requires Java 21)
1. Typecheck: `./gradlew typecheck` (or `bun run typecheck`).
2. Frontend tests (must pass unchanged):
- `./gradlew test --tests "ai.kilocode.client.session.ui.PromptPanelTest"`
- `./gradlew test --tests "ai.kilocode.client.session.ui.prompt.KiloPromptCompletionProviderTest"`
- In particular these must stay green: `test prompt local completion shortcut opens mention lookup`,
`test prompt local completion shortcut opens slash lookup`, `test prompt completion lookup is
positioned above caret`, `test accepted file mention highlights immediately`.
3. Manual (run IDE): typing `@` and a leading `/` opens the popup; Ctrl+Space opens it; the popup renders
above the caret. Open DevKit → `Frontend and Backend API Usage` (and `Unstable API Usage`): the
frontend completion/lookup usages should no longer be flagged; the backend `search` usages are
acknowledged via `@Suppress` + comment.
## Acceptance criteria
- `PromptPanel.kt` (production) contains no references to `CodeCompletionHandlerBase`, completion
`CompletionType`, or `com.intellij.codeInsight.lookup.impl.LookupImpl`.
- `bindLookup` casts to `LookupEx`; lookup positioning behavior unchanged.
- `KiloWorkspaceRpcApiImpl.search` is `@Suppress("UnstableApiUsage")` with the explanatory comment;
`searchFiles` catches `LinkageError` and degrades.
- Typecheck and the listed tests pass; no test files were modified.
## Risk + fallback
- Primary risk: performing `ACTION_CODE_COMPLETION` via the `DataContext` lambda does not open the
lookup in the headless `PromptPanelTest` (e.g. the action's `update()` disables itself).
- Fallback (only if Task 1 tests fail): keep the action approach but additionally force autopopup —
in the editor settings provider (`addSettingsProvider { ed -> ... }`) add
`ed.putUserData(com.intellij.codeInsight.AutoPopupController.ALWAYS_AUTO_POPUP, true)`, and implement
`showCompletion` as `com.intellij.codeInsight.AutoPopupController.getInstance(project).scheduleAutoPopup(ed)`.
`ALWAYS_AUTO_POPUP` is a public `Key` and `scheduleAutoPopup(Editor)` is public (this is exactly how the
platform's `TextFieldWithCompletion(forceAutoPopup = true)` works). This is async, so rely on the
tests' existing `waitForLookupItems` polling.
@@ -1,134 +0,0 @@
# JetBrains prompt completion follow-up: cancellation noise and explicit shortcut
## Goal
Finish the JetBrains prompt completion fix by addressing the two issues reported after the first implementation pass:
1. Typing `@...` rapidly logs repeated `workspace file search failed ... IndicatorCancellationException` warnings.
2. Pressing the code-completion shortcut (`Control-Space` in the user's keymap) in the prompt editor does not reliably invoke slash/mention completion.
The existing no-match placeholder, fast-typing lookup retention, and mid-token token detection changes stay in place.
## Current state
Already implemented in Kilo-owned JetBrains files:
- `KiloPromptCompletionProvider` adds a non-destructive `No matches` info row for slash and mention no-results.
- Slash and mention result sets call `restartCompletionOnAnyPrefixChange()`.
- Mention indexing row uses the same no-op info-row behavior.
- `PromptPanel` sets `AutoPopupController.ALWAYS_AUTO_POPUP` on the prompt editor.
- `token()` now recognizes the full whitespace-bounded token around the caret, while `prefix` remains the text before the caret.
- Tests cover no-match placeholders, accepting placeholders, real matches, and mid-token explicit completion through the provider fixture.
## New findings
### Cancellation warnings while typing
The warning stack is expected IntelliJ completion cancellation, not a real backend failure:
- `KiloPromptCompletionProvider.search()` runs `service.searchFiles(...)` inside `runBlockingCancellable` during completion calculation.
- IntelliJ cancels stale completion work on each prefix change while the user types.
- IntelliJ source confirms `IndicatorCancellationException` extends `java.util.concurrent.CancellationException`, and `ProcessCanceledException` extends Kotlin/JVM `CancellationException`.
- `KiloWorkspaceService.searchFiles()` currently catches broad `Exception`, logs it as a warning, and returns `FileSearchResultDto()`.
- IntelliJ source explicitly says `ProcessCanceledException` should not be logged or swallowed.
Conclusion: `searchFiles()` must not log completion/progress cancellation. It should rethrow cancellation before the generic failure catch so the completion infrastructure can discard stale results.
### `Control-Space` in the prompt editor
Provider tests prove `TextCompletionContributor` can invoke `KiloPromptCompletionProvider`, but the real prompt editor shortcut path can diverge:
- `EditorTextField` and its content component already publish `CommonDataKeys.EDITOR`, so editor data context is probably not the primary issue.
- IntelliJ `BaseCodeCompletionAction` uses `ActionRemoteBehavior.FrontendOtherwiseBackend`.
- In remote/split frontend mode, completion can fall back to backend execution or use frontend RD completion filtering.
- IntelliJ `CompletionContributor.forLanguage()` filters contributors to `FrontendCompletionContributor` when RD frontend completion is enabled; platform `TextCompletionContributor` is not marked as such.
- Our prompt completion provider is frontend UI state installed through `TextCompletionUtil`, so relying on the global code-completion action is fragile in split/RD mode.
Conclusion: keep using the existing prompt-local `showCompletion(editor)` path, but bind the active code-completion shortcut directly to it for the prompt editor component. This keeps the behavior frontend-local and avoids the RD contributor/action fallback path.
## Implementation plan
### 1. Stop logging stale completion cancellation
File: `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt`
- In `searchFiles(directory, query, limit)`, add a cancellation catch before the generic catch:
```kotlin
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
LOG.warn(...)
FileSearchResultDto()
}
```
- Use the JVM/Kotlin cancellation type that catches both IntelliJ `IndicatorCancellationException` and `ProcessCanceledException` on this target.
- Do not add logging for cancellation.
- Leave non-cancellation failures unchanged: still warn and return `FileSearchResultDto()`.
- Do not catch cancellation inside `KiloPromptCompletionProvider.search()`; let completion infrastructure cancel stale calculations and avoid caching a cancelled result.
Tests:
- Add `KiloWorkspaceServiceTest` coverage where `FakeWorkspaceRpcApi.search` throws `CancellationException` and `service.searchFiles(...)` rethrows it.
- Keep existing behavior testable for non-cancellation failures if a small fake failure case is easy to add, but do not expand the scope unnecessarily.
### 2. Bind explicit completion shortcut locally in `PromptPanel`
File: `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt`
- Add a prompt-local action field, for example a private `AnAction?`, that calls `showCompletion(editor)` for the initialized prompt editor.
- When the editor is configured in `addSettingsProvider`, install this action on `ed.contentComponent` using the current shortcut set from `IdeActions.ACTION_CODE_COMPLETION`:
```kotlin
val base = ActionManager.getInstance().getAction(IdeActions.ACTION_CODE_COMPLETION)
completionAction.registerCustomShortcutSet(base.shortcutSet, ed.contentComponent)
```
- Use `DumbAwareAction.create { ... }` or a small private action object; keep it local to `PromptPanel`.
- Only install this action when `completion != null`.
- Unregister the action in `removeNotify()` with `unregisterCustomShortcutSet(ed.contentComponent)` to avoid retaining disposed editor components.
- On keymap changes for `IdeActions.ACTION_CODE_COMPLETION`, unregister and reinstall so custom keymaps are respected. The existing `bindKeymap()` listener already watches shortcut changes; extend that path.
- Keep the existing document-listener trigger for inserted `@` and leading `/`, because native auto-popup still does not fire for those trigger chars.
- Keep `showCompletion()` unchanged for now, including the `ONLY_ABOVE` lookup positioning. It already uses the frontend-local handler path the manual `@`/`/` trigger relies on.
Tests:
- Add `PromptPanelTest` coverage with a real `PromptPanel` plus `KiloPromptCompletionProvider`:
- Realize the panel.
- Set the prompt editor text to `@dep` and place the caret at the end.
- Configure `rpc.searchResult` with `src/deploy.ts`.
- Invoke the registered prompt-local completion action and assert the active lookup contains `src/deploy.ts`.
- Repeat or add a slash case for `/ne` showing `new`.
- Prefer invoking the registered action via `ActionUtil.getActions(editor.contentComponent)` and an `AnActionEvent` built from the editor component data context. This verifies the component-local shortcut binding without depending on brittle headless key-event dispatch.
- If action ordering produces multiple registered actions, identify the prompt-local action by shortcut set and by invoking only actions registered on the editor content component after `PromptPanel` initialization.
### 3. Re-run focused verification
From `packages/kilo-jetbrains/`:
- `./gradlew :frontend:test --tests ai.kilocode.client.session.ui.prompt.KiloPromptCompletionProviderTest`
- `./gradlew :frontend:test --tests ai.kilocode.client.session.ui.PromptPanelTest`
- `./gradlew :frontend:test --tests ai.kilocode.client.app.KiloWorkspaceServiceTest`
- `./gradlew typecheck`
Known pre-existing caveat:
- Full `./gradlew test` was previously failing with unrelated frontend `NoSuchMethodError` failures in existing UI constructor tests. Do not treat that as introduced by this completion fix unless it changes shape after these edits.
## Manual verification
In a sandbox IDE:
- Type `@d`, `@de`, `@dep` quickly. No `workspace file search failed ... IndicatorCancellationException` warnings should appear.
- Type an unmatched mention such as `@zzzz`; the popup should stay open with `No matches`.
- Put the caret in or after `@dep` and press the code-completion shortcut; mention completions should open.
- Type `/ne` and press the code-completion shortcut; slash completions should open.
- Confirm the popup still appears above the prompt editor for manual `@` and leading `/` triggers.
## Scope and constraints
- Touch only Kilo-owned JetBrains plugin files; no `kilocode_change` markers are needed.
- Do not introduce a new global completion contributor unless the prompt-local shortcut binding proves insufficient.
- Do not rewrite completion UI into a custom popup for this pass.
- Avoid swallowing IntelliJ cancellation exceptions; cancellation is control flow, not a user-visible failure.
@@ -1,162 +0,0 @@
# Fix JetBrains empty `@` mention completion (shows only "Searching files…")
Scope: Kilo-owned frontend code under
`packages/kilo-jetbrains/frontend/.../session/ui/prompt/`. No shared/upstream files, so
no `kilocode_change` markers needed.
## Symptom (from user)
- Type `@` with no further typing → the lookup shows only `Searching files…` and never
fills with the root file/dir entries.
- Typing past `@` (e.g. `@backend`) works and shows results. "The rest works ok."
- Doing `@` a **second time** shows entries.
## Root cause
`KiloPromptCompletionProvider.mention("")` (`KiloPromptCompletionProvider.kt:165-188`)
runs a **non-blocking** search via `search("")` (`:190-199`):
- `cache[""]` is empty on the first `@`, so it launches a debounced background fetch
(`refresh()`, `:207-220`) and renders the `Searching files…` placeholder.
- When the fetch returns it stores `cache[""]` and posts `onRefresh()` →
`PromptPanel.restartCompletion()` (`PromptPanel.kt:561-569`) which calls
`CompletionServiceImpl.currentCompletionProgressIndicator?.scheduleRestart()` to repaint
the open lookup in place.
The **second** `@` works because `cache[""]` is now populated, so `search("")` returns
synchronously (`:191`) and the lookup is filled on first paint with no async restart.
So the failing case is exactly the one that depends on the async
`onRefresh → restartCompletion → scheduleRestart()` repaint of an already-open lookup,
and that in-place repaint is the fragile/internal-API path. Typing past `@` does **not**
depend on it: each keystroke routes through the platform's own
`LookupTypedHandler.prefixUpdated()` → `scheduleRestart()`
(`$INTELLIJ_REPO/platform/lang-impl/.../lookup/impl/LookupTypedHandler.java:176-179`),
which re-runs `fillCompletionVariants` and warms/serves the cache reliably. The empty `@`
is the only path with no keystroke to drive that robust restart.
### Investigation note (why we are not relying on a "race" theory)
`@` is not a letter/digit/`_`, so `CompletionAutoPopupHandler.checkAutoPopup` returns
CONTINUE for it (`.../editorActions/CompletionAutoPopupHandler.java:43-50`) and
`TypedAutoPopupImpl` only auto-pops on `.`/`/`/contributor opt-in
(`.../editorActions/TypedAutoPopupImpl.java:30-37`). Typing `@` therefore never triggers
the platform autopopup; the popup is opened solely by our explicit `showCompletion`
(`PromptPanel.kt:551-559`, `invokeCompletion(project, ed, 1)` → `invocationCount == 1` →
`isAutopopupCompletion() == false`, `CompletionProgressIndicator.java:842-844`). So the
empty-`@` failure is **not** an autopopup-vs-explicit race on `@`. Removing
`ALWAYS_AUTO_POPUP` is correct hygiene but is not, by itself, a guaranteed fix — so we add
a deterministic cache prewarm.
## Plan
### Part 1 — Prewarm `cache[""]` (deterministic fix)
Make the first `@` take the same path that already works the second time: serve the root
listing synchronously from a warm cache.
**1a. Add `prewarm()` to `KiloPromptCompletionProvider`.**
```kotlin
fun prewarm() {
if (cache.containsKey("")) return
scope.launch {
val result = service.searchFiles(workspace.directory, "", 50)
// Only pin a settled, useful result. Skip empty/failed/indexing results so a
// later `@` retries instead of caching a transient "nothing" state.
if (result.files.isNotEmpty() || result.git) cache.putIfAbsent("", result)
}
}
```
- Launches on the existing `scope` (off-EDT, satisfies `searchFiles`' not-EDT
requirement). No unit-test-mode early return, so it is directly testable.
- `cache[prefix]` is checked before everything else in `search()`
(`KiloPromptCompletionProvider.kt:191`), so once `cache[""]` is warm the first `@`
renders root entries on first paint with no placeholder and no async restart.
- Does not pin `indexing`/empty/failed results (`searchFiles` returns
`FileSearchResultDto()` on failure), avoiding a stuck "No matches"/empty cache.
**1b. Re-warm at the right lifecycle points in `PromptPanel`.**
- In `setReady(true)` (`PromptPanel.kt:304-308`): call `completion?.prewarm()` — the
workspace is ready so the backend can search. This warms the cache before the user's
first `@`.
- In `clear()` (`PromptPanel.kt:383-391`), after `completion?.clearMentions()` (which
empties the cache, `:68-76`): call `completion?.prewarm()` so the next `@` after a send
is warm again.
`prewarm()` is idempotent (`containsKey("")` guard), so repeated `setReady(true)` /
`clear()` calls are cheap.
### Part 2 — Remove `ALWAYS_AUTO_POPUP` (requested restructure)
Drive completion solely through the explicit triggers we already own and the active
lookup's own prefix-change restart, so every completion in a session is explicit
(`invocationCount == 1`, non-autopopup) and `scheduleRestart()` always takes the
non-autopopup async path.
**2a. Remove** `ed.putUserData(AutoPopupController.ALWAYS_AUTO_POPUP, true)`
(`PromptPanel.kt:172`) and the now-unused `import com.intellij.codeInsight.AutoPopupController`
(`:23`).
Why this is safe (verified against `$INTELLIJ_REPO`):
- Opening: `triggerCompletion` opens the popup on a single-char `@` (anywhere) and a
leading `/` (`PromptPanel.kt:537-549`); the bound completion shortcut also calls
`showCompletion` (`installCompletionShortcut`, `:571-584`).
- Narrowing while open: typing routes through `LookupTypedHandler.prefixUpdated()` →
`scheduleRestart()` and `restartCompletionOnAnyPrefixChange()`
(`KiloPromptCompletionProvider.kt:152,166`) — independent of `ALWAYS_AUTO_POPUP`.
- All completions stay non-autopopup: `showCompletion` uses `invocationCount = 1`, and
restarts inherit it (`phase.indicator?.invocationCount`,
`.../completion/CompletionPhase.kt:339`).
- Net effect: removes wasted autopopup completion attempts on ordinary (non-`@`/`/`)
typing and guarantees the async `scheduleRestart()` never routes through the gated
autopopup path (`shouldSkipAutoPopup` / activity guards, `CompletionPhase.kt:285,326-333`).
No behavior the tests depend on changes: `KiloPromptCompletionProviderTest` and
`PromptPanelTest` drive completion via `myFixture.completeBasic()` / the explicit
"Kilo Prompt Completion" action (`invokeCompletionAction`), neither of which uses
`ALWAYS_AUTO_POPUP`.
## Tests
- `KiloPromptCompletionProviderTest` (provider constructed directly, no panel → `prewarm`
is only called explicitly by the test, so `searchQueries` assertions in other tests are
unaffected). Add:
- **prewarm warms the empty-prefix cache:** set `rpc.searchResult` with a couple of root
entries, call `provider.prewarm()`, `waitFor { rpc.searchQueries.contains("") }`, then
`complete("@<caret>")` and assert the lookup contains those entries **and**
`assertEquals(listOf(""), rpc.searchQueries)` (the `@` was served from the prewarmed
cache, no second query).
- Existing `test blank mention completion includes special and root entries` and the
`searchQueries` expectations stay green (no prewarm there).
- `PromptPanelTest`: existing completion/highlight tests must stay green. `setReady(true)`
/ `clear()` already appear in many tests; confirm none assert `searchQueries` (they do
not) so the added prewarm fetch is harmless. Optionally add a panel-level check that the
prewarm fetch is issued after `setReady(true)`.
## Files to change
- `frontend/.../session/ui/prompt/KiloPromptCompletionProvider.kt` — add `prewarm()`.
- `frontend/.../session/ui/prompt/PromptPanel.kt` — call `prewarm()` from `setReady(true)`
and `clear()`; remove `ALWAYS_AUTO_POPUP` and its import.
- `frontend/src/test/.../prompt/KiloPromptCompletionProviderTest.kt` — prewarm test.
## Verification
From `packages/kilo-jetbrains/`: `./gradlew :frontend:test` (targeted to the two prompt
test classes) and `./gradlew typecheck` (Java 21). Manual check in `./gradlew runIde`:
open a session, type `@` and wait — root files/dirs appear immediately (no stuck
`Searching files…`); type `@backend` — narrows correctly; send a prompt, type `@` again —
still immediate.
## Decisions / tradeoffs
1. **Prewarm is the primary, deterministic fix.** It reproduces the working "second time"
path on the first `@` and does not depend on the fragile in-place async restart.
2. **`ALWAYS_AUTO_POPUP` removal is included as requested** and is safe hygiene, but the
investigation shows `@` does not go through the platform autopopup, so it is paired with
prewarm rather than relied on alone.
3. Only settled, useful (`files.isNotEmpty() || git`) results are pinned in `cache[""]` to
avoid caching transient empty/indexing/failed states.
@@ -1,60 +0,0 @@
# JetBrains File Mention Rendering Parity
## Goal
Make JetBrains user-message file mentions render like VS Code: show the user's original prompt and file reference, do not show the CLI's synthetic read-tool payload (`<path>`, `<type>`, `<content>`) as visible user text.
## Findings
- VS Code renders user messages through `packages/kilo-vscode/webview-ui/src/components/chat/VscodeUserMessage.tsx` and `packages/kilo-ui/src/components/message-part.tsx`.
- VS Code chooses the first non-synthetic text part for the visible prompt: `part.type === "text" && !part.synthetic`.
- VS Code keeps file parts available for user-message rendering, but text-file mentions are not shown as raw read output.
- The CLI expands text file parts in `packages/opencode/src/session/prompt.ts` into synthetic text parts containing read-tool input/output plus the original file part.
- JetBrains currently drops the `synthetic` part field in `PartDto` / `KiloCliDataParser.parsePart`, so synthetic read output is converted into normal `Text` and rendered by `MessageView` / `PromptView`.
- JetBrains already has `PromptAttachmentView` for user `FileAttachment` parts, but it cannot distinguish mention-derived text files from explicit text-file attachments because `PromptPartDto` does not carry `source` metadata.
## Implementation Plan
1. Preserve CLI part metadata in JetBrains DTOs.
- Add `synthetic: Boolean? = null` to `PartDto` in `packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt`.
- Add small serializable source DTOs for file-part source metadata, e.g. `PartSourceDto` and `PartSourceTextDto`.
- Add `source: PartSourceDto? = null` to both `PartDto` and `PromptPartDto`.
2. Parse and serialize the new fields in the backend bridge.
- Update `KiloCliDataParser.parsePart(...)` to read top-level `synthetic` and `source` from CLI message parts.
- Update `KiloCliDataParser.buildPromptPartJson(...)` to include `source` for outgoing file prompt parts.
- Keep the existing `sanitizeUserPromptText(...)` fallback for older or malformed events that do not include `synthetic`.
- Add parser tests for `synthetic`, source parsing, and prompt JSON source serialization.
3. Send source metadata for JetBrains mentions.
- Update `SessionUi.mentionParts(...)` so each `@path` file mention includes `source = { type: "file", path, text: { value: "@path", start, end } }`.
- Update `dataPart(...)` call sites for `@terminal` and `@git-changes` to include equivalent source text metadata when feasible.
- Use the exact mention token offsets from the submitted prompt so downstream rendering can distinguish mention-backed context from manually attached files.
4. Hide synthetic user text in the JetBrains session model/rendering path.
- Add `synthetic` to the `Text` model or otherwise track hidden synthetic part IDs in `SessionModel`.
- In `SessionModel.updateContent(...)` and `loadHistory(...)`, skip user `text` parts where `dto.synthetic == true`.
- Ensure a later update that marks an existing user text part synthetic removes the visible content.
- Ensure deltas for already-hidden synthetic part IDs do not create a visible prompt part.
- Do not hide assistant synthetic text unless there is an explicit existing behavior to match.
5. Match VS Code's text-file mention display more closely.
- Extend `FileAttachment` with optional source metadata.
- In `MessageView`, do not add source-backed `text/plain` user file parts to `PromptAttachmentView`; keep source-less text files as cards so explicit attachments still render.
- Keep image/PDF user attachments as attachment cards, matching VS Code's separate attachment preview behavior.
- Optional polish if scope allows: make source-backed `@file` tokens in `PromptView` clickable via `openFile(source.path)` using existing markdown/link styling. Do this only if it stays small and does not require rebuilding the retained Swing view architecture.
6. Add focused tests.
- `KiloCliDataParserTest`: parse `synthetic: true`; parse file `source`; build prompt JSON with source; history preserves source and synthetic flags.
- `SessionModelTest`: user synthetic text is skipped in history and live updates; assistant text is unchanged; synthetic deltas do not create visible text after a synthetic update.
- `SessionUiUpdateTest`: a user message containing original text, a synthetic read-output text part, and a source-backed text/plain file shows only the prompt text and no raw `<path>/<content>` text; source-less text/plain attachments still render as `PromptAttachmentView`.
7. Release note and verification.
- Add a patch changeset for the JetBrains plugin: "Hide raw file contents from mentioned files in JetBrains chat messages."
- Run the smallest relevant checks from `packages/kilo-jetbrains/`: targeted Gradle tests for parser/model/UI update tests, then `./gradlew typecheck` if the targeted tests pass.
## Non-Goals
- Do not change `packages/opencode/src/session/prompt.ts`; the CLI already marks these parts as synthetic.
- Do not add brittle string stripping for the full `<path>/<type>/<content>` block as the main fix. Metadata-based filtering matches VS Code and avoids hiding user-authored text.
- Do not remove attachment cards for manually attached text files that are not source-backed mentions.
-96
View File
@@ -1,96 +0,0 @@
# JetBrains Mention Actions Organization Plan
## Goal
Organize known JetBrains prompt mentions the same way as built-in slash actions, so built-in mention names and metadata are centralized instead of hardcoded across completion, highlighting, and prompt part conversion.
## Current Findings
- There is one known built-in mention today: `@git-changes`.
- File mentions are dynamic backend search results from `KiloWorkspaceService.searchFiles(...)`; they are not built-in mention actions and should stay dynamic.
- `KiloPromptCompletionProvider` hardcodes `git-changes` in three places:
- Completion availability and lookup insertion when `FileSearchResultDto.git` is true.
- Highlighting as a valid mention without requiring a tracked file path.
- Validation skip so it is not resolved as a file path.
- `PromptMentionParts.kt` hardcodes the same token, output filename, and resource URI in `gitChangesPart(...)`.
- `SessionUi.mentionParts(...)` hardcodes `@git-changes` before calling `gitChangesPart(...)`.
- Tests assert `git-changes` and `@git-changes` literals in completion, highlighting, prompt panel, and prompt part conversion tests.
## Relevant Files
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProvider.kt`
- Known mention completion, highlighting, validation skip, and lookup element construction.
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptMentionParts.kt`
- Converts `@git-changes` into a synthetic text attachment from the git diff.
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt`
- Detects the git changes mention before fetching git diff.
- `packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties`
- Existing localization key `prompt.mention.gitChanges`.
- `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProviderTest.kt`
- Completion and highlighting tests for `git-changes`.
- `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/PromptMentionPartsTest.kt`
- Prompt part conversion tests for `@git-changes`.
- `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt`
- Editor highlighting and highlighter lifecycle tests involving `@git-changes`.
## Implementation Plan
1. Add a top-level `MentionAction` class in the prompt package:
- Path: `frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/MentionAction.kt`
- Package: `ai.kilocode.client.session.ui.prompt`
- Shape should mirror `SlashAction` where practical, but without an executable callback:
- `name`
- `description`
- `hints`
- `available(search: FileSearchResultDto): Boolean` or equivalent, so availability rules live with known mention metadata
- Add an inner `Spec` class for built-in metadata:
- `name`
- `descriptionKey`
- `hints`
- `filename` for resource-backed prompt parts where applicable
- `uri` for resource-backed prompt parts where applicable
- `available(search: FileSearchResultDto): Boolean` for completion visibility
- Add `GIT_CHANGES` and ordered `ALL` constants.
- Add small token helpers if useful, for example `Spec.token` returning `@name`, to avoid reconstructing token literals in multiple files.
2. Bind mention specs where localized descriptions are needed:
- Add a small binder close to completion construction, analogous to slash action binding, or inside `KiloPromptCompletionProvider` if that keeps the constructor simpler.
- Prefer passing `mentions: List<MentionAction>` into `KiloPromptCompletionProvider` if this keeps known mention completion explicit and testable.
- Keep `KiloBundle.message(...)` resolution in frontend/UI code, not in static metadata.
3. Update `KiloPromptCompletionProvider` to use known mention actions:
- Add a `mentions` constructor parameter if using explicit binding.
- Replace hardcoded highlight checks with a known mention name set.
- Replace validation skip literal checks with the known mention name set.
- Replace completion logic for `git-changes` with iteration over known mentions whose names or hints match the prefix and whose `available(search)` condition is satisfied.
- Preserve current availability behavior: show `git-changes` only when `search.git` is true.
- Preserve ordering: known mentions before file results, with the existing priority behavior so `git-changes` remains first for blank mention completion when available.
- Preserve no-match behavior: no placeholder if a known mention matches; otherwise placeholder only when there are no matching known mentions and no file results.
4. Update prompt part conversion to use centralized metadata:
- Replace `"@git-changes"`, `"git-changes"`, and `"git-changes.txt"` in `PromptMentionParts.kt` with `MentionAction.GIT_CHANGES` metadata.
- Keep the function name `gitChangesPart(...)` because the implementation still specifically converts git diff text.
- Preserve the boundary check that ignores `@git-changes-foo`.
5. Update `SessionUi.mentionParts(...)`:
- Replace `text.contains("@git-changes")` with the centralized token from `MentionAction.GIT_CHANGES`.
- Keep diff fetching lazy and only when the prompt contains the known mention token.
6. Update tests:
- Replace hardcoded built-in mention names with `MentionAction.GIT_CHANGES.name` or `MentionAction.GIT_CHANGES.token` where practical.
- Keep test strings readable where the full prompt text is the important fixture, but derive expected values from `MentionAction` to reduce duplicated metadata.
- Add or adjust one provider test to assert known mention metadata is used for completion description if there is an existing practical assertion path; otherwise do not add brittle UI presentation assertions.
## Non-Goals
- Do not turn dynamic file mentions into `MentionAction` entries.
- Do not add new built-in mentions.
- Do not change `@git-changes` behavior, availability, inserted text, highlighting, validation, prompt part source metadata, or output filename.
- Do not rename `gitChangesPart(...)` unless the implementation later supports multiple resource mention conversions.
## Verification
- Run targeted frontend tests from `packages/kilo-jetbrains`:
- `./gradlew :frontend:test --tests "ai.kilocode.client.session.ui.prompt.KiloPromptCompletionProviderTest" --tests "ai.kilocode.client.session.ui.prompt.PromptMentionPartsTest" --tests "ai.kilocode.client.session.ui.PromptPanelTest"`
- Run JetBrains typecheck from `packages/kilo-jetbrains`:
- `./gradlew typecheck`
@@ -1,172 +0,0 @@
# Fix flickering `@`-mention / `/`-slash prompt completion (JetBrains)
## Goal
Stop the prompt completion lookup from flickering as the user types an `@`-mention,
without changing the inline-dropdown UX. Keep reusing IntelliJ's standard completion
Lookup and the backend `GotoFileModel` ("Go to File") search engine that already power
the feature. This is **Option 1** from the investigation (smallest change, fully
Kilo-owned code, no UX change).
## Background / root cause
All touched code lives under Kilo-owned paths (`ai/kilocode/client/...`), so **no
`kilocode_change` markers are needed**.
Current flow:
- `PromptPanel` editor uses `TextCompletionUtil.DocumentWithCompletionCreator(provider, autoPopup=true)`.
- `PromptPanel.triggerCompletion()` manually invokes completion when `@` or a leading `/` is typed
(`PromptPanel.kt:457-478`), forcing `LookupPositionStrategy.ONLY_ABOVE`.
- `KiloPromptCompletionProvider.mention()` (`KiloPromptCompletionProvider.kt:73-93`) runs a
**blocking RPC search per completion pass** (`runBlockingCancellable { service.searchFiles(...) }`)
and applies a local `PlainPrefixMatcher(prefix)`.
- Backend `KiloWorkspaceRpcApiImpl.search()` (`KiloWorkspaceRpcApiImpl.kt:317-367`) uses IntelliJ's
`GotoFileModel` for fuzzy/camel-hump ranking.
Why it flickers (collapse → re-query → refill cycle every keystroke):
1. Backend does fuzzy/camel matching; the frontend re-filters with `PlainPrefixMatcher`
(substring match), which **hides server results that don't literally contain the typed
substring** (e.g. `sfb` → `src/foo/Bar.kt`).
2. A blank query returns `emptyList()` from the backend, so right after `@` only the
git-changes/terminal specials show; the first typed char tends to empty the locally
filtered list.
3. `restartCompletionWhenNothingMatches()` only re-queries **after** the list is already
empty, so the user sees empty → (async RPC gap) → refill on nearly every keystroke.
4. Variable RPC latency makes the refill land at inconsistent times.
## The change (precise edits)
### 1. `KiloPromptCompletionProvider.mention()` — let the server own filtering
File: `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProvider.kt`
- Replace `result.restartCompletionWhenNothingMatches()` with
`result.restartCompletionOnAnyPrefixChange()` so the platform keeps the lookup open and
deterministically re-runs the query on every prefix change (replacing items in place)
instead of waiting for the list to empty.
- Use an **accept-everything matcher** for the mention path so the platform never hides the
server's ranked results: wrap with `PlainPrefixMatcher.ALWAYS_TRUE`
(`com.intellij.codeInsight.completion.PlainPrefixMatcher`) instead of
`applyPrefixMatcher(result, prefix)`.
Sketch:
```kotlin
private fun mention(prefix: String, result: CompletionResultSet) {
result.restartCompletionOnAnyPrefixChange()
val out = result.withPrefixMatcher(PlainPrefixMatcher.ALWAYS_TRUE)
val search = searchFiles(prefix) // see step 2 (cache)
// ...unchanged: specials (git-changes, terminal), indexing advertisement, files...
}
```
Notes:
- Keep `applyPrefixMatcher` (the `TextCompletionProvider` override) as-is — it is still used
by the `slash` path, where a static command list should be filtered locally. Only the
mention path overrides the matcher to accept-all. `TextCompletionContributor` calls
`applyPrefixMatcher` before `fillCompletionVariants`, so re-wrapping inside `mention`
with `ALWAYS_TRUE` is the correct seam.
- Items are added in server-weight order; with `ALWAYS_TRUE` there is no client-side
highlight/filter, which is intended (server is the source of truth).
### 2. Small last-prefix cache to cut redundant RPC (optional but recommended)
Still in `KiloPromptCompletionProvider`. With `restartCompletionOnAnyPrefixChange`, the
platform re-invokes the provider on each change and `runBlockingCancellable` cancels stale
passes. Add a **1-entry cache** keyed by `prefix` so identical prefixes (e.g. delete +
retype, or a duplicate pass for the same prefix) don't re-hit RPC:
```kotlin
@Volatile private var cached: Pair<String, FileSearchResultDto>? = null
private fun searchFiles(prefix: String): FileSearchResultDto {
cached?.takeIf { it.first == prefix }?.let { return it.second }
val result = runBlockingCancellable { service.searchFiles(workspace.directory, prefix, 50) }
cached = prefix to result
return result
}
```
- Clear `cached` in `clearMentions()` (called from `PromptPanel.clear()`) to avoid stale
results across prompt resets.
- Keep it a single entry; do not build an unbounded map.
### 3. Verify the `@` trigger does not double-fire
File: `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt`
- `autoPopup=true` plus the manual `triggerCompletion()` could both fire on the first `@`.
During implementation, confirm (via `runIde`) whether typing `@` briefly shows a double
session. If it does, prefer keeping the **manual** trigger (autopopup is unreliable for
non-identifier chars like `@`) and confirm the second `scheduleAutoPopup` is a no-op while
a lookup is already active. Only adjust if a real double-flash is observed — otherwise
leave `triggerCompletion`/`showCompletion` and the `ONLY_ABOVE` strategy unchanged.
- Keep `LookupPositionStrategy.ONLY_ABOVE` (added in `7fd80d0af9`) — it prevents the popup
from flipping above/below as item counts change.
## Edge cases / considerations
- **Empty prefix right after `@`**: backend returns no files (blank query), so only
git-changes/terminal specials show until the user types. This is acceptable and not part
of the flicker bug; showing recent files for an empty prefix is a separate enhancement and
is **out of scope**.
- **Indexing state**: the existing `search.indexing` branch (advertisement + single info
element, early return) is unchanged.
- **Slash path**: unchanged. It must keep local `PlainPrefixMatcher` filtering of the static
command list and must NOT call `restartCompletionOnAnyPrefixChange`.
- **Threading**: `searchFiles` stays a cancellable blocking call inside the completion
background pass (as today). No EDT work added.
## Testing
Add `KiloPromptCompletionProviderTest` (frontend test, `BasePlatformTestCase`), modeled on
existing `KiloWorkspaceService(scope, FakeWorkspaceRpcApi())` usage
(e.g. `KiloWorkspaceServiceTest`, `SessionUiTestBase`) and `workspaces.workspace("/test")`.
Use `myFixture` to drive the real completion machinery against a plain-text document with the
provider installed via `TextCompletionUtil.installProvider(file, provider, true)`:
- **No local collapse of fuzzy results**: set `FakeWorkspaceRpcApi.searchResult` to files
whose paths do NOT contain the typed substring (simulating camel/fuzzy matches, e.g.
query `sfb` returning `src/foo/Bar.kt`). After completing on `@sfb<caret>`, assert all
server files appear in `myFixture.lookupElementStrings` (proves `ALWAYS_TRUE` matcher).
- **Re-query on prefix change**: extend `FakeWorkspaceRpcApi.searchFiles` to **record the
query string** (and optionally return per-query results). Assert that changing the prefix
triggers a new `searchFiles` call for the new prefix (proves
`restartCompletionOnAnyPrefixChange`).
- **Cache**: assert a repeated identical prefix does not issue a second `searchFiles` call,
and that `clearMentions()` resets the cache.
- **Specials still surface**: with `searchResult.git = true` / `terminal = true`, assert
`@git-changes` / `@terminal` appear for matching prefixes.
Test-support change required: add query recording to `FakeWorkspaceRpcApi.searchFiles`
(e.g. `val searchQueries = mutableListOf<String>()` and optionally a
`searchByQuery: (String) -> FileSearchResultDto` override). This is in the `testing` package
and acceptable.
If the full `myFixture.complete` lookup proves too fiddly for deterministic assertions, fall
back to asserting the visible lookup element strings after `myFixture.type(...)` of each
character, which is the user-facing behavior we care about. Do not add production methods
that exist only for test access.
## Verification
- Unit/integration: `./gradlew test` (or targeted `./gradlew test --tests '*KiloPromptCompletionProviderTest*'`)
from `packages/kilo-jetbrains/`. Requires Java 21 (`java -version`; install via
`sdk install java 21-tem && sdk use java 21-tem` if missing).
- Typecheck: `./gradlew typecheck` from `packages/kilo-jetbrains/`.
- Manual: `./gradlew runIde`, open the Kilo tool window, type `@` then progressively type a
fuzzy filename; confirm the lookup stays open and updates in place with no empty→refill
flash, and that selecting a file inserts `@<path>`.
## Out of scope / fallback
- No switch to the Search Everywhere / Go-to-File chooser (Option 2) and no custom
`JBPopup` + `JBList` (Option 3). If, after this change, RPC latency still dominates and the
lookup is not smooth enough, escalate to Option 3 (custom popup with debounced async list
updates) as a follow-up.
- No changes to backend `search()` / `GotoFileModel` ranking.
- No new behavior for empty-prefix `@` (recent files).
@@ -1,243 +0,0 @@
# Make JetBrains `@`/`/` prompt completion as reliable as editor completion
Scope: Kilo-owned frontend code under
`packages/kilo-jetbrains/frontend/.../session/ui/prompt/`. No shared/upstream files, so
no `kilocode_change` markers needed.
This is a follow-up to `jetbrains-empty-mention-completion.md` (prewarm +
`ALWAYS_AUTO_POPUP` removal, already implemented). It fixes the remaining instability.
## Symptoms (from user)
- Open `@`, then **type fast** (e.g. `@backend`) → the lookup **disappears**.
- **Type slowly** → it stays and narrows fine.
- The popup also **flickers** and **shows below the caret for a moment** before settling
above.
## Root cause (verified against `$INTELLIJ_REPO`)
The provider fetches results **asynchronously on its own coroutine scope** and then forces
the open lookup to repaint via a **manual restart**:
- `KiloPromptCompletionProvider.search()` (`KiloPromptCompletionProvider.kt:198-207`)
returns an **empty placeholder** immediately for a cold prefix and launches a debounced
`refresh()` (`:215-228`).
- When `refresh()` finishes it stores `cache[prefix]` and posts `onRefresh()`
(`:224-225`) → `PromptPanel.restartCompletion()` (`PromptPanel.kt:561-569`) →
`CompletionProgressIndicator.scheduleRestart()`.
`scheduleRestart()` does not repaint in place. It `cancel()`s the current indicator and
schedules a brand-new completion through the `CommittingDocuments` phase
(`CompletionProgressIndicator.java:913-941` → `CompletionPhase.kt:234-262`). That async
path commits documents in a non-blocking read action and then, on the EDT, runs
`startAsyncCompletionIfNotExpired` (`CompletionPhase.kt:294-340`). It contains:
```kotlin
if (phase.myTracker.hasAnythingHappened() && (phase.indicator == null || !phase.indicator.lookup.isShown)) {
phase.cancelPhase()
CompletionServiceImpl.setCompletionPhase(NoCompletion) // <- popup disappears
return
}
```
So when our async `onRefresh → scheduleRestart` lands **while the user is still typing**
(`ActionTracker.hasAnythingHappened()` is true) and the lookup is in a transient
not-`isShown` window (it is, because our cold-prefix path produced only the transient
"Searching files…" element), the phase is dropped to `NoCompletion` and the lookup is
gone. Slow typing avoids the overlap, which is why it only reproduces under fast typing.
Why the editor's own completion does **not** disappear under fast typing: every keystroke
restart goes through the same code, but the editor's contributors return their items
**synchronously inside the background completion calculation**, so the lookup is already
`isShown` with real items and the `hasAnythingHappened()` branch is skipped (the comment
on `CompletionPhase.kt:327-328` says a shown lookup "is going to handle close by itself").
Our extra, arbitrarily-timed manual restart is the thing that races.
The "shows below then jumps above" flicker is a related but separate timing bug:
`showCompletion` sets `LookupPositionStrategy.ONLY_ABOVE` **after** `invokeCompletion`
returns (`PromptPanel.kt:555-558`), i.e. after the lookup has already been created and
positioned at the default `PREFER_BELOW` (`LookupPresentation.kt:24`). The disappear →
recreate cycle makes this visible repeatedly.
### Key platform facts that make the fix safe
1. **Completion contributors run off the EDT, on a background thread, inside a cancellable
read action.** `CodeCompletionHandlerBase.startContributorThread`
(`CodeCompletionHandlerBase.java:414-438`) runs `indicator.runContributors` via
`AsyncCompletion.startThread` (`CompletionThreading.kt:118-153`,
`Dispatchers.IO.limitedParallelism(5)`). The indicator picks `AsyncCompletion` whenever
completion is not invoked inside a write action
(`CompletionProgressIndicator.java:200-202`). Therefore a **blocking** `searchFiles`
call inside `fillCompletionVariants`, guarded by `runBlockingCancellable`, is the
platform-intended pattern (same as index-backed file/class completion) — it does not
touch the EDT, and `FakeWorkspaceRpcApi.searchFiles` already asserts not-EDT, which the
existing unit-test path satisfies today via `fetch()`.
2. **The platform already drives "background updates" of an open lookup.** While the
contributor runs, the lookup shows the native calculating state and the
`CompletionConsumer` adds items to the live lookup as they are produced. This is the
mechanism to "reuse" — we do not need our own debounce/refresh/scheduleRestart.
3. **Restart reuses the same lookup instance.** `obtainLookup`
(`CodeCompletionHandlerBase.java:274-298`) returns the existing completion lookup via
`markReused()`, so a `LookupPresentation` set once persists across prefix-change
restarts.
4. **The lookup is activated before it is shown.** `ClientLookupManager.createLookup`
fires `LookupManagerListener.activeLookupChanged(null, lookup)`
(`ClientLookupManager.kt:91-93`) before `indicator.showLookup()`. Setting the position
strategy in that listener positions the lookup correctly on first paint — no flash.
5. `restartCompletionOnAnyPrefixChange()` (`KiloPromptCompletionProvider.kt:160,174`) is
what makes the platform re-run our contributor (and thus a fresh search) for each new
prefix. Keep it; it is the supported per-keystroke restart trigger.
## Plan
### Part 1 — Compute results synchronously on the completion background thread
Make the production path do what the unit-test path already does: block on the backend
search (with cancellation) inside `fillCompletionVariants`, and let the platform manage
the lookup. Delete the async refresh + manual restart entirely.
In `KiloPromptCompletionProvider.kt`:
- **Replace `search()`** (`:198-207`) with a cache-or-fetch that always returns a settled
result:
```kotlin
private fun search(prefix: String): FileSearchResultDto = cache[prefix] ?: fetch(prefix)
```
`fetch()` (`:209-213`) stays as-is: `runBlockingCancellable { service.searchFiles(...) }`
+ `cache[prefix] = result`. It now runs on the async completion thread in production too.
- **Delete** `refresh()` (`:215-228`), the `onRefresh` field (`:48-49`), the `job`/`want`
`@Volatile` fields (`:42-46`), and `SEARCH_DELAY_MS` (`:51-53`).
- **Delete** the `Search` data class (`:322`) and the `pending` plumbing. Simplify
`mention()` (`:173-196`): drop the `search.pending` branch and the
`prompt.mention.searching` placeholder; keep the `indexing` branch and the `noMatches`
fallback. The native lookup spinner now covers the "in flight" state.
- **`clearMentions()`** (`:68-76`): keep clearing `paths/exists/pending/cache`; drop
`want`/`job` resets.
- Remove now-unused imports: `kotlinx.coroutines.Job`, `kotlinx.coroutines.delay`,
`com.intellij.openapi.application.ApplicationManager` (was only used for the
unit-test-mode branch and the `invokeLater` in `refresh`). Keep `launch`/`CoroutineScope`
(used by `prewarm` and `validate`) and `runBlockingCancellable`.
- Keep `prewarm()` and `restartCompletionOnAnyPrefixChange()` unchanged. Prewarm keeps the
first `@` instant (cache hit, no blocking); per-prefix restarts hit the cache when the
prefix repeats and otherwise block briefly with native cancellation.
In `PromptPanel.kt`:
- **Delete** the `completion?.onRefresh = { ... restartCompletion() }` wiring in `init`
(`:250-252`), the `completion?.onRefresh = null` line in `removeNotify` (`:451`), and the
`restartCompletion()` method (`:561-569`).
- Remove the now-unused import
`com.intellij.codeInsight.completion.impl.CompletionServiceImpl` (`:25`).
Remove the unused `prompt.mention.searching` key from
`frontend/src/main/resources/messages/KiloBundle.properties:167` (no locale file
references it). Keep `prompt.mention.indexing`.
### Part 2 — Position the lookup above the caret before first paint
Set `ONLY_ABOVE` as soon as the completion lookup for our editor is created, via a
project-level `LookupManagerListener`, instead of after `invokeCompletion`.
In `PromptPanel.kt`:
- Add a `private var lookupBus: MessageBusConnection? = null` and a `bindLookup()` that
subscribes once to `LookupManagerListener.TOPIC` on `project.messageBus`:
```kotlin
connection.subscribe(LookupManagerListener.TOPIC, LookupManagerListener { _, next ->
val lookup = next as? LookupImpl ?: return@LookupManagerListener
if (lookup.editor !== editor.getEditor(false)) return@LookupManagerListener
lookup.presentation = LookupPresentation.Builder(lookup.presentation)
.withPositionStrategy(LookupPositionStrategy.ONLY_ABOVE)
.build()
})
```
Call `bindLookup()` from `addNotify()` (next to `bindRoot()`/`bindKeymap()`); disconnect
in `removeNotify()` (`lookupBus?.disconnect(); lookupBus = null`).
- **Simplify `showCompletion()`** (`:551-559`) to just open completion
(`invokeCompletion(project, ed, 1)`); drop the post-invoke presentation block — the
listener now owns positioning for both the `@`/`/` document-triggered open and the
completion-shortcut open, and it persists across restarts (reused lookup).
- Add import `com.intellij.codeInsight.lookup.LookupManagerListener`. Existing
`LookupManager`, `LookupImpl`, `LookupPresentation`, `LookupPositionStrategy` imports stay
(now used by the listener).
## Tests
`KiloPromptCompletionProviderTest` (provider built directly; `myFixture.completeBasic()`
runs the real off-EDT completion calculation):
- All existing tests stay green. They already exercise the blocking `fetch()` path (the old
`isUnitTestMode` branch) and assert exact `rpc.searchQueries`; `completeBasic()` issues a
single search per invocation, so query counts are unchanged. `test prewarm serves blank
mention completion from cache` and `test mention completion reuses identical prefix
result` still hold (cache hit ⇒ no extra query); `test clearing mentions resets cached
prefix result` still holds (cache cleared ⇒ refetch).
- The `prompt.mention.searching` placeholder is no longer produced; confirm no test asserts
it (none do — only `noMatches()` and `indexing` are asserted/relevant).
`PromptPanelTest` (real editor + lookup via `invokeCompletionAction` + `waitForLookupItems`):
- Existing `test prompt local completion shortcut opens mention lookup` / `... slash lookup`
stay green and now cover the production blocking path end to end.
- **Add** `test prompt completion lookup is positioned above caret`: open the mention
lookup as in the existing shortcut test, then assert
`(LookupManager.getActiveLookup(editor) as LookupImpl).presentation.positionStrategy ==
LookupPositionStrategy.ONLY_ABOVE`.
Note on the disappear/flicker race: it is an EDT/timing interaction that cannot be asserted
deterministically in a unit test. Coverage is the deterministic position test above plus
the existing real-lookup tests; the fast-typing regression is validated manually in
`./gradlew runIde`.
## Files to change
- `frontend/.../session/ui/prompt/KiloPromptCompletionProvider.kt` — blocking
`search()`; delete `refresh`/`onRefresh`/`job`/`want`/`SEARCH_DELAY_MS`/`Search`/pending;
simplify `mention()`; trim imports.
- `frontend/.../session/ui/prompt/PromptPanel.kt` — delete `onRefresh` wiring and
`restartCompletion()`; add `LookupManagerListener` for `ONLY_ABOVE`; simplify
`showCompletion()`; adjust imports.
- `frontend/src/main/resources/messages/KiloBundle.properties` — remove
`prompt.mention.searching`.
- `frontend/src/test/.../prompt/KiloPromptCompletionProviderTest.kt` /
`.../session/ui/PromptPanelTest.kt` — keep existing green; add position test.
## Verification
From `packages/kilo-jetbrains/`:
- `./gradlew :frontend:test --tests
ai.kilocode.client.session.ui.prompt.KiloPromptCompletionProviderTest --tests
ai.kilocode.client.session.ui.PromptPanelTest`
- `./gradlew typecheck`
- Manual (`./gradlew runIde`): open a session, `@` then type `@backend` **fast** → lookup
stays open and narrows (no disappear); it opens **above** the caret with no below-flash;
`/` commands behave the same; send a prompt and repeat.
## Decisions / tradeoffs / risks
1. **Block on the background completion thread; drop the manual restart.** This is the
idiomatic platform pattern and removes the `onRefresh → scheduleRestart` race that
causes the disappear. It also removes our 120 ms debounce and the dual code paths
(test vs prod), unifying behavior.
2. **Chatty-RPC tradeoff.** Each prefix change re-runs the search. In practice superseded
calculations are cancelled by the platform (`runBlockingCancellable` throws on cancel,
`searchFiles` rethrows `CancellationException`), the per-prefix `cache` dedupes repeats,
and `prewarm` covers the empty prefix. Backend file search is cheap, so this is
acceptable and matches how index-backed completion behaves.
3. **Read lock held during the search.** The contributor runs inside a cancellable read
action; a localhost search blocks it briefly. `BgCalculation.restartOnWriteAction`
(`CompletionPhase.kt:446-470`) cancels the indicator the moment the user types (write
action), promptly releasing the lock. This is the same contract index-backed
contributors rely on.
4. **Indexing/transient results.** With no auto-refresh, if the backend reports `indexing`
the user sees the "Indexing…" message until the next keystroke or reopen re-queries
(rare, brief). `prewarm` already avoids pinning indexing/empty results in `cache[""]`.
5. **Experimental/internal APIs.** `LookupPresentation` / `LookupPositionStrategy` are
`@ApiStatus.Experimental` and `LookupImpl` is impl-level — already used here. The new
`LookupManagerListener` is a stable public topic. Revisit on platform upgrades (the
existing `showCompletion`/`restartCompletion` comments already flag this).
-214
View File
@@ -1,214 +0,0 @@
# Fix JetBrains prompt mention completion + highlighting
Scope: Kilo-owned frontend code under
`packages/kilo-jetbrains/frontend/.../session/ui/prompt/`. No shared/upstream files,
so no `kilocode_change` markers needed.
## Symptoms (from user)
Completion:
- Type `@` then quickly `ba` → the completion popup **disappears**.
- Typing slowly works, but there is a visible delay/rebuild between keystrokes.
Highlighting:
- After accepting a file from completion, the mention is **not** highlighted as valid
until you type a space.
- Editing the tail of a valid mention so it becomes invalid does **not** turn it red.
User goals: less blinking on rebuilds, do not hide the popup while typing between
rebuilds, correct path validation.
## Root cause analysis
### Why the popup disappears (completion)
`KiloPromptCompletionProvider.mention()` calls `search(prefix)` which runs the backend
file search **synchronously** via `runBlockingCancellable` inside
`fillCompletionVariants` (`KiloPromptCompletionProvider.kt:166-171`). It also calls
`result.restartCompletionOnAnyPrefixChange()`, so every typed character restarts the
whole completion.
IntelliJ's restart path (`CompletionPhase.scheduleAsyncCompletion` →
`startAsyncCompletionIfNotExpired`, verified in
`$INTELLIJ_REPO/platform/lang-impl/.../CompletionPhase.kt:326-333`) cancels a scheduled
completion when **activity happened in the editor AND the previous lookup is not yet
shown**:
```
if (phase.myTracker.hasAnythingHappened() && (phase.indicator == null || !phase.indicator.lookup.isShown)) {
phase.cancelPhase(); setCompletionPhase(NoCompletion); return // popup gone, not rescheduled
}
```
So while the first blocking `search()` is still running (lookup not shown yet), typing a
second character cancels the session entirely and nothing reopens it. Slow typing leaves
enough time for the blocking search to finish and the lookup to show; once a lookup is
shown, later restarts are kill-safe (`!lookup.isShown` is false). This exactly matches
"fast = disappears, slow = works". The blocking RPC is the core problem.
The pattern (blocking + `restartCompletionOnAnyPrefixChange`) is what LSP uses
(`LspCompletionContributor.kt:44-48`), but LSP also suffers the same issue for slow
servers. We must make the **first paint non-blocking** so the lookup shows immediately.
### Why blinking happens on rebuilds
Each keystroke fully restarts completion, re-queries the backend (blocking), and tears
down/rebuilds the item list — even though we already have usable results for a shorter
prefix. Empty/"No matches" can flash mid-refresh.
### Why highlighting is wrong
Highlights only refresh on **document change** (`PromptPanel` document listener →
`syncHighlights()`, `PromptPanel.kt:241-249,377-403`). Two consequences:
1. **Not valid after accept:** In `replace()` (`KiloPromptCompletionProvider.kt:228-241`)
the path is added to `paths`/`exists` **after** `ctx.document.replaceString(...)`. The
synchronous `documentChanged` → `syncHighlights()` therefore runs while `paths` is
still empty, so the just-inserted mention is not highlighted. The next edit (typing a
space) re-syncs and it finally turns valid.
2. **Doesn't turn red after editing tail:** `highlights()` suppresses any mention while
the caret is inside it (`under -> Unit`, `KiloPromptCompletionProvider.kt:84-90`), and
`validate()` skips tokens under the caret (`:99`). When the caret leaves the token via
click/arrow (no document change), nothing re-syncs, so validation never runs and the
now-invalid mention is never flagged. Typing a space forces a re-sync, which is the
only reason it eventually works.
## Plan
### Part A — Highlighting (low risk, do first)
**A1. Valid immediately after accepting a completion.**
In `KiloPromptCompletionProvider.replace()`, move the `paths.add(path)` /
`exists[path] = true` update to **before** `ctx.document.replaceString(...)`. The
synchronous document-change → `syncHighlights()` then sees the new mention and highlights
it as `MENTION` right away. No behavior change to final text or `mentionPaths()`, so
existing `replace`-related tests still pass.
**A2. Re-validate when the caret leaves a mention token.**
Add a `CaretListener` to the prompt editor (in `PromptPanel`'s `addSettingsProvider`
block alongside the existing focus listener, `PromptPanel.kt:178-186`) that calls
`syncHighlights()` when the caret crosses a mention-span boundary. Guard it so it only
re-syncs when the set of "caret is inside a mention" actually changes (compare previous
vs new offset against `mentionSpans`) to avoid churn on ordinary cursor movement. This
makes an edited-then-left mention run `validate()` and turn red (or back to valid).
Register on the per-editor `caretModel`; it is released with the editor, consistent with
the existing focus listener.
### Part B — Completion popup (the disappearing/blinking fix)
Make mention completion **non-blocking** and serve results from a cache, refreshing in
the background. Slash completion is already synchronous/in-memory and is left unchanged.
**B1. Replace the single `cached` field with a small cache + async machinery** in
`KiloPromptCompletionProvider`:
- `private val cache = ConcurrentHashMap<String, FileSearchResultDto>()` (thread-safe;
`fillCompletionVariants` runs on a background completion thread, the refresh writes from
a coroutine).
- `@Volatile private var job: Job? = null` (single in-flight fetch; cancel on new
request — debounced).
- `@Volatile private var want: String? = null` (latest requested prefix, to ignore stale
fetch completions).
- `var onRefresh: (() -> Unit)? = null` — set by `PromptPanel` to restart the active
completion when fresh data arrives.
- `clearMentions()` clears `cache`, cancels `job`, resets `want` (keeps existing reset
semantics so the "clearing resets cached prefix result" test still does two queries).
**B2. Non-blocking `mention(prefix, result)`** decision order:
1. `cache[prefix] != null` → render those results (exact, instant).
2. Else pick the **longest cached key that is a prefix of `prefix`** as a stale base and
render its results with the existing `PlainPrefixMatcher.ALWAYS_TRUE` (instant, keeps
the lookup populated and shown); then request a background refresh for `prefix`.
3. Else (truly cold, no base) → render a "searching" placeholder via the existing
`info()` element (`NEVER_AUTOCOMPLETE`, prefix-preserving) so the lookup still shows;
request a background refresh.
Because fill never blocks, the lookup always shows on the first paint → the platform's
"activity happened & lookup not shown" kill path can no longer fire → the popup stops
disappearing on fast typing. Keep `restartCompletionOnAnyPrefixChange()` so each
keystroke re-runs this cheap fill (re-evaluates cache/stale and kicks the next refresh).
**B3. Background refresh (debounced) + in-place restart.**
- `refresh(prefix)`: set `want = prefix`; cancel previous `job`; launch on `scope`:
optional small `delay(~120ms)` debounce, then `service.searchFiles(dir, prefix, 50)`,
store in `cache[prefix]`, and if `prefix == want` post `onRefresh()` on the EDT.
- `PromptPanel` sets `completion.onRefresh = { restartActiveCompletion() }` where
`restartActiveCompletion()` is `@RequiresEdt`, checks `!project.isDisposed`, and — only
if a lookup is active on this editor — restarts it **in place** (replaces items without
closing/reopening) using the platform completion indicator. This is the existing
documented "uses IntelliJ impl/internal completion APIs; revisit on platform upgrades"
boundary already present for `showCompletion` (`PromptPanel.kt:533-541`); reuse the same
caveat comment. Guard to the current editor (`indicator.editor === ed`).
This yields: instant first paint, stale results held across restarts (no empty/"No
matches" flash → less blinking), one debounced backend query per settled prefix, and an
in-place refresh when results arrive.
**B4. Unit-test mode stays synchronous.**
`fillCompletionVariants`/`mention` is invoked through `myFixture.completeBasic()` in the
provider tests, which expect results synchronously. When
`ApplicationManager.getApplication().isUnitTestMode()` is true, perform the cold fetch
inline (`runBlockingCancellable`, current behavior) and skip the async
refresh/`onRefresh` path. This keeps every existing `KiloPromptCompletionProviderTest`
and `PromptPanelTest` completion test green with no rewrites, while production uses the
async path. (`isUnitTestMode()` branching is the platform's own convention in completion
code, e.g. `scheduleAutoPopup`/`handleEmptyLookup`.) Documented as a deliberate tradeoff
in the open questions.
**B5. Optional refinement — avoid double-trigger blink.**
`@`/`/` triggers an explicit `showCompletion` (`PromptPanel.triggerCompletion`/
`showCompletion`) while the editor also has `AutoPopupController.ALWAYS_AUTO_POPUP`, so
two completions can race on the first character. Guard `showCompletion` to no-op if a
lookup is already active for the editor. Keep as a follow-up tweak; the main fix is B1–B4.
### New bundle key
Add `prompt.mention.searching=Searching files…` to
`frontend/src/main/resources/messages/KiloBundle.properties` (English only; other locales
fall back). Used by the cold "searching" placeholder.
## Tests
- `KiloPromptCompletionProviderTest`: unchanged and must stay green (B4 keeps them
synchronous). Add:
- cache base reuse: after `complete("@back")` then `complete("@backend")`, the second
serves stale base results immediately (assert no regression in `searchQueries`
expectations as designed).
- `PromptPanelTest`:
- **Accept → valid immediately (A1):** type `@dep`, open lookup, accept `src/deploy.ts`,
and assert the mention span has `DefaultLanguageHighlighterColors.METADATA`
**without** typing a space (use `waitForSend`).
- **Edit tail → invalid on caret leave (A2):** set a valid mention (`fileResolver`
returns it), edit the tail to an unknown path, move the caret out of the token, and
assert `CodeInsightColors.WRONG_REFERENCES_ATTRIBUTES` appears via `waitForSend`.
- Existing highlight/lookup tests must remain green.
## Files to change
- `frontend/.../session/ui/prompt/KiloPromptCompletionProvider.kt` — A1 reorder; B1–B4
cache/async/refresh; cold placeholder.
- `frontend/.../session/ui/prompt/PromptPanel.kt` — A2 caret listener; wire
`completion.onRefresh` + `restartActiveCompletion()`; optional B5 guard.
- `frontend/src/main/resources/messages/KiloBundle.properties` — `prompt.mention.searching`.
- Tests: `frontend/src/test/.../prompt/KiloPromptCompletionProviderTest.kt`,
`frontend/src/test/.../ui/PromptPanelTest.kt`.
## Verification
From `packages/kilo-jetbrains/`: `./gradlew typecheck` and `./gradlew test` (Java 21).
Manual check in `./gradlew runIde`: type `@` then quickly `ba` (popup stays, narrows to
backend entries); accept a file (highlights valid immediately); break a valid mention's
tail and click away (turns red).
## Decisions
1. **In-place restart (B3):** use the platform completion indicator (the same internal
boundary already used by `showCompletion`, with the existing "revisit on platform
upgrades" caveat comment). Lowest blink; replaces items without closing the lookup.
2. **Tests (B4):** branch on `ApplicationManager.getApplication().isUnitTestMode()` so the
cold fetch runs inline in tests (existing tests stay green, no rewrites); production
uses the async path.
## Remaining tunable
- **Debounce delay** for the background refresh: starting at ~120ms; tune during
implementation/manual testing.
@@ -1,60 +0,0 @@
# Fix JetBrains Mention File Search Refresh
## Diagnosis
`@deploy.sh` can fail even when IntelliJ Find by Name can locate the file because mention completion currently performs backend file search only once, at the moment completion is invoked.
Current flow:
- Typing `@` explicitly invokes completion in `PromptPanel`.
- `KiloPromptCompletionProvider.mention()` receives an empty prefix and calls `searchFiles(workspace.directory, "", 50)`.
- The backend returns the first limited slice of indexed file names, not all repo files.
- As the user types `deploy.sh`, `acceptChar()` returns `ADD_TO_PREFIX`, so IntelliJ appends the typed chars to the active lookup prefix and filters the already-returned lookup items.
- The backend is not queried again unless completion is restarted.
- If `deploy.sh` was not in the initial empty-prefix result slice, it can never appear.
Secondary backend issue:
- `KiloWorkspaceRpcApiImpl.search()` uses `NameUtil.buildMatcher("*${query.trim()}")`.
- This behaves like a suffix-ish matcher rather than a broad contains/fuzzy search for partial file names.
- A partial query like `deploy` may not match `deploy.sh` until the extension is typed, depending on matcher semantics.
Platform finding:
- IntelliJ exposes public restart hooks on `CompletionResultSet`: `restartCompletionOnPrefixChange(...)`, `restartCompletionOnAnyPrefixChange()`, and `restartCompletionWhenNothingMatches()`.
- For `TextCompletionProvider`, these can be called from `fillCompletionVariants(...)`; no internal completion APIs are needed.
- Relevant IntelliJ source:
- `TextCompletionContributor.java`: computes prefix and delegates to the provider.
- `TextCompletionCharFilter.java`: `ADD_TO_PREFIX` keeps the lookup alive and updates prefix.
- `LookupTypedHandler.java`: typed chars only restart completion when requested by result-set restart hooks.
- `CompletionResultSet.java`: public restart methods.
## Implementation Plan
1. Update `KiloPromptCompletionProvider.mention()` to request backend re-query as the mention prefix changes.
- Prefer `result.restartCompletionOnAnyPrefixChange()` for file mentions, or `restartCompletionOnPrefixChange(StandardPatterns.string().longerThan(0))` if empty-prefix churn is undesirable.
- Also call `result.restartCompletionWhenNothingMatches()` as a fallback so missed initial slices recover as the user narrows the prefix.
- Keep slash command behavior static unless slash commands show a similar issue; slash options are small enough to filter locally.
2. Improve backend filename matching in `KiloWorkspaceRpcApiImpl.search()`.
- Use a contains/fuzzy pattern such as `*${query.trim()}*` instead of `*${query.trim()}`.
- Treat blank query separately to avoid pathological wildcard behavior and keep initial `@` results capped.
- Continue sorting by matching degree, path length, then path.
3. Add a targeted test if practical in the JetBrains test suite.
- Unit-test the provider behavior only if existing tests can invoke text completion without excessive platform setup.
- Otherwise, add coverage around backend search matching if there is already a backend/RPC test harness.
- Avoid mocks where possible; use actual implementation helpers if accessible.
4. Verify.
- Run `./gradlew typecheck` from `packages/kilo-jetbrains/`.
- If tests are added, run the smallest relevant Gradle test task for the touched module.
## Expected Result
Typing `@deploy.sh` should re-run backend search as the prefix changes, so indexed repo files like `deploy.sh` appear even when they were not included in the initial empty-`@` result set.
## Notes
- The file still must be under the current workspace directory and visible to the IntelliJ project index.
- If standard IntelliJ Find by Name sees the file and it is under the workspace root, mention completion should see it after this fix.
@@ -1,59 +0,0 @@
# JetBrains Mention Icons And Priority Plan
## Goal
- Show IntelliJ file type icons for file mention completions instead of the generic text icon.
- Keep predefined mention completions, currently `@git-changes`, above file/folder entries in the blank `@` default list.
## Current State
- `KiloPromptCompletionProvider.file(...)` uses `AllIcons.Nodes.Folder` for directories and `AllIcons.FileTypes.Text` for every non-directory file.
- `AttachmentOpeners.attachmentIcon(...)` already uses `FileTypeManager.getInstance().getFileTypeByFileName(name).icon ?: AllIcons.FileTypes.Text`, which is a suitable public IntelliJ API pattern to reuse.
- `mention(...)` adds `git-changes` before files, but the completion sorter can still influence presentation. Explicit priority will make the intended ordering stable.
- The backend/RPC DTO already provides `WorkspaceFileDto.name` and `directory`, so no shared DTO or backend changes are needed.
## Implementation
1. Update `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProvider.kt`.
2. Import `com.intellij.openapi.fileTypes.FileTypeManager` and `com.intellij.codeInsight.lookup.PrioritizedLookupElement`.
3. Add a small helper for mention file icons:
- Return `AllIcons.Nodes.Folder` when `file.directory` is true.
- Otherwise return `FileTypeManager.getInstance().getFileTypeByFileName(file.name).icon ?: AllIcons.FileTypes.Text`.
- Use `file.name`, not `file.path`, so file type lookup uses the final path segment consistently.
4. Change `file(file)` to call the icon helper instead of hardcoding `AllIcons.FileTypes.Text`.
5. Add a small helper for predefined mention priority:
- Wrap predefined mention lookup elements with `PrioritizedLookupElement.withPriority(..., 100.0)`.
- Keep file lookup elements unprioritized.
- This should apply to `@git-changes` whenever it is included, including the blank `@` list and non-empty matching prefixes.
6. Keep the existing backend search behavior unchanged, including root folder/file listing for blank queries and fuzzy search for non-empty queries.
## Tests
Update `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProviderTest.kt`.
1. Extend the blank mention completion test to assert ordering:
- `lookupElementStrings.orEmpty().take(3)` should be `listOf("git-changes", "src", "README.md")` when the fake backend returns `git = true` and root entries.
2. Add coverage for file type icons:
- Complete a file with a recognizable extension, e.g. `image.png`.
- Render the corresponding lookup element into `LookupElementPresentation`.
- Assert the icon equals `FileTypeManager.getInstance().getFileTypeByFileName("image.png").icon ?: AllIcons.FileTypes.Text`.
3. Keep or add directory icon coverage if cheap:
- Assert a directory entry renders `AllIcons.Nodes.Folder`.
## Verification
Run from `packages/kilo-jetbrains/`:
```bash
./gradlew :frontend:test --tests ai.kilocode.client.session.ui.prompt.KiloPromptCompletionProviderTest
./gradlew typecheck
```
## Release Note
- This is user-facing JetBrains behavior. Add a patch changeset such as `.changeset/<slug>.md` for `@kilocode/kilo-jetbrains` with a concise user-facing description, for example: `Show file type icons and keep predefined mentions first in JetBrains mention completions.`
## Risks
- File type icons depend on installed IntelliJ file type registrations. Tests should compare against the platform API result instead of assuming a specific concrete icon for an extension.
- Completion ordering can be affected by IntelliJ sorters. Use `PrioritizedLookupElement` for predefined mentions instead of relying only on insertion order.
@@ -1,263 +0,0 @@
# JetBrains prompt input: undo, clean completion, not-found mention highlight
Improve the JetBrains plugin chat prompt input (mentions `@` and slash `/`). Three
independent fixes, all in the `frontend` module under
`packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/`.
These are all Kilo-owned files (path contains `kilocode`/`client` and is not shared with
upstream opencode), so **no `kilocode_change` markers are needed**.
## Background (verified)
- The prompt editor is `PromptEditorTextField` → `SessionEditorTextField` → platform
`com.intellij.ui.EditorTextField`, hosted in the chat tool window (`PromptPanel`).
- The mention/slash completion + highlight logic lives in
`KiloPromptCompletionProvider`. Highlights are applied to the editor by
`PromptPanel.syncHighlights()` via `markupModel` range highlighters.
- The CLI resolves *configured* references from prompt text; arbitrary file mentions are
surfaced client-side. This change is **highlight + insertion only** — it does not change
what gets attached/sent (`mentionParts`/`mentionFileParts` stay as-is).
---
## Issue 1 — Undo/redo (Cmd-Z) does nothing in the prompt
### Root cause
The platform `$Undo`/`$Redo` actions (`UndoRedoAction`) resolve the target editor from
`PlatformCoreDataKeys.FILE_EDITOR`. When focus is in the prompt editor, the focused
component is `EditorComponentImpl`, whose `uiDataSnapshot` provides `CommonDataKeys.EDITOR`
but **not** `FILE_EDITOR`. With a null `FileEditor`, `UndoDocumentUtil.getDocRefs(null)`
returns an empty list, so the undo manager has no document references and reports "nothing
to undo".
JetBrains' own `EditorTextField`-based inputs fix this by exposing a `FileEditor` wrapper.
See `platform/collaboration-tools/.../CodeReviewCommentTextFieldFactory.kt:114`
(`sink[PlatformCoreDataKeys.FILE_EDITOR] = TextEditorProvider.getInstance().getTextEditor(editor)`,
commented "required for undo/redo").
### Change
File: `session/ui/editor/SessionEditorTextField.kt`
In `uiDataSnapshot`, additionally expose a `TextEditor` wrapper of the current editor:
```kotlin
override fun uiDataSnapshot(sink: DataSink) {
super.uiDataSnapshot(sink)
ctx?.let { sink.set(PromptDataKeys.SEND, it) }
getEditor(false)?.let { editor ->
sink.set(
PlatformCoreDataKeys.FILE_EDITOR,
TextEditorProvider.getInstance().getTextEditor(editor),
)
}
}
```
New imports: `com.intellij.openapi.actionSystem.PlatformCoreDataKeys`,
`com.intellij.openapi.fileEditor.impl.text.TextEditorProvider`.
Notes:
- `TextEditorProvider.getTextEditor(editor)` caches the wrapper in the editor's user data,
so this is cheap and stable across snapshots.
- This is the same wrapper platform `BasicUiDataRule` would synthesize; setting it
explicitly guarantees undo/redo regardless of data-rule timing, and matches the
collaboration-tools precedent.
- Applies to both prompt and the question custom-answer editor (both use
`SessionEditorTextField`); both benefit from working undo.
---
## Issue 2 — Completing a mention mid-token leaves garbage
### Reproduction
Input `@backend/deploy⟨caret⟩-dev.sh`, accept the `backend/deploy-dev.sh` suggestion →
result is `@backend/deploy-dev.sh -dev.sh` (leftover suffix).
### Root cause
`KiloPromptCompletionProvider.replace()` (lines ~210-220) computes the replaced region end
as `ctx.tailOffset` — the end of the platform-inserted lookup string — instead of the end
of the whitespace-delimited token under the caret. The original suffix after the caret
(`-dev.sh`) is therefore left behind.
### Change
File: `session/ui/prompt/KiloPromptCompletionProvider.kt`
Compute `end` as the end of the full whitespace-delimited token (scan right from the token
start to the next whitespace), mirroring the existing left-scan used for `start` and the
boundary logic in `token()`:
```kotlin
private fun replace(ctx: InsertionContext, value: String, trim: Boolean, path: String? = null) {
val text = ctx.document.text
val offset = ctx.startOffset.coerceAtMost(text.length)
val start = (offset - 1 downTo 0).firstOrNull { text[it].isWhitespace() }?.plus(1) ?: 0
val end = (start until text.length).firstOrNull { text[it].isWhitespace() } ?: text.length
val next = text.getOrNull(end)
val insert = if (trim && next?.isWhitespace() == true) value.trimEnd() else value
ctx.document.replaceString(start, end, insert)
ctx.editor.caretModel.moveToOffset(start + insert.length)
path?.let { paths.add(it); exists[it] = true } // exists seeding added in Issue 3
}
```
This fixes file mentions (`file`), special mentions (`special`/`@git-changes`), and server
slash commands (`server`), which all route through `replace`. End-of-token (normal)
completion behaves exactly as before.
Apply the same token-end computation to the "No matches" placeholder insert handler
(`info`, lines ~158-166) so accepting it mid-token also restores the prefix cleanly instead
of duplicating the suffix. Extract a tiny shared helper for the token end (e.g.
`tokenEnd(text, start)`) used by both `replace` and `info`.
Behavior holds for `\n` (Enter) and Tab acceptance — both run the same insert handler.
---
## Issue 3 — A mention whose file is not found is not highlighted (stays plain text)
Chosen approach (confirmed with user): **accurate** — validate each `@mention` against the
workspace via the backend and show a red error highlight only when the file truly does not
resolve. The token currently under the caret (being typed) is never flagged.
### Current behavior
`highlights()` only emits `MENTION` for completion-tracked `paths` (+ `git-changes`).
Every other `@token` (hand-typed or edited) renders as ordinary text, with no signal that
it will not resolve.
### Design
Add asynchronous, cached existence validation using the existing
`KiloWorkspaceService.files(directory, path)` RPC (returns matches or empty list; runs
off-EDT — the fake asserts `assertNotEdt("files")`).
#### `KiloPromptCompletionProvider.kt`
- Constructor: add `private val scope: CoroutineScope` (for launching validation).
- New state:
- `private val exists = Collections.synchronizedMap(mutableMapOf<String, Boolean>())`
- `private val pending = Collections.synchronizedSet(mutableSetOf<String>())`
- `clearMentions()`: also clear `exists` and `pending`.
- `replace(...)`: when a completed path is accepted, seed `exists[path] = true` (known-good,
highlights instantly with no RPC) — see Issue 2 snippet.
- New `HighlightKind.INVALID` enum value.
- New private `mentionSpans(text): List<Span>` — whitespace-delimited tokens that start with
`@` and have a non-empty value (returns `start`, `end`, `value`). Replaces the current
substring/longest-first scan with direct token scanning (inherently non-overlapping).
- `highlights(text: String, caret: Int = -1): List<Highlight>` (add `caret` param, default
`-1` keeps existing call sites/tests working):
- Command highlight: unchanged.
- For each mention span (`underCaret = caret in span.start..span.end`):
- `value == "git-changes"` → `MENTION`
- `value in paths || exists[value] == true` → `MENTION`
- `underCaret` → skip (no highlight; being edited — avoids red flashing while typing)
- `exists[value] == false` → `INVALID`
- else (unknown / validation pending) → skip
- New `fun validate(text: String, caret: Int, onResolved: () -> Unit)`:
- For each mention span where `value != "git-changes"`, `value !in paths`, not `underCaret`,
`value !in exists`, `value !in pending`:
- `pending.add(value)`
- `scope.launch { val ok = runCatching { service.files(workspace.directory, value).isNotEmpty() }.getOrDefault(false); exists[value] = ok; pending.remove(value); onResolved() }`
- `onResolved` is only invoked when an RPC actually completes (no launch → no callback →
no refresh loop). EDT marshalling is the caller's responsibility.
#### `PromptPanel.kt`
- `syncHighlights()`:
- Read `val caret = ed.caretModel.offset`.
- Call `provider.validate(ed.document.text, caret) { ApplicationManager.getApplication().invokeLater { if (!project.isDisposed) refreshHighlights() } }`.
- Pass `caret` to `provider.highlights(ed.document.text, caret)`.
- Re-running `syncHighlights()` from the refresh callback is safe: all values are now
cached or pending, so `validate` launches nothing and the callback chain terminates.
- Add error attributes key + mapping:
- companion: `private val INVALID_KEY = CodeInsightColors.WRONG_REFERENCES_ATTRIBUTES`
(red "unresolved reference" attributes — semantically a not-found file reference).
- `key(kind)`: add `INVALID -> INVALID_KEY` branch.
- New import: `com.intellij.openapi.editor.colors.CodeInsightColors`.
Notes / non-goals:
- Validation is best-effort and cached; files created/deleted after a check may show a stale
state until re-typed. Acceptable for an input affordance.
- Attachment semantics (`mentionParts` / `mentionFileParts`, the tracked `paths` set used at
send time) are **unchanged**. This issue is purely the pre-send highlight.
---
## Tests
All tests use `BasePlatformTestCase` (real Application + EDT), per package conventions.
Update the two `completion()` / provider constructions to pass the new `scope` argument:
- `KiloPromptCompletionProviderTest.setUp` (provider construction)
- `PromptPanelTest.completion()` helper
- `SessionUi` line ~336 passes `scope = cs`.
`FakeWorkspaceRpcApi`: add a `var fileResolver: ((String) -> List<WorkspaceFileDto>)? = null`
and make `files()` return `fileResolver?.invoke(path) ?: fileMatches`, so a single test can
make one path resolve and another not. (Kilo test file — no markers.)
### `KiloPromptCompletionProviderTest.kt`
- **Mid-token clean insert (new):** configure `@backend/deploy⟨caret⟩-dev.sh`, search returns
`backend/deploy-dev.sh`, accept via `myFixture.type('\n')`, assert document is
`"@backend/deploy-dev.sh "` and `mentionPaths()` contains `backend/deploy-dev.sh`.
- **Mid-token clean insert with trailing content (new):** `@backend/deploy⟨caret⟩-dev.sh tail`
→ `"@backend/deploy-dev.sh tail"` (trailing space trimmed against existing whitespace).
- **Not-found mention → INVALID (new):** `fileResolver` returns empty for `unknownPath`; call
`validate("see @unknownPath", caret = -1) { ... }`, `waitFor` until resolved, then assert
`highlights("see @unknownPath", -1)` contains `Highlight(4, 16, INVALID)`.
- **Existing hand-typed mention → MENTION (new):** `fileResolver` returns a match for
`src/x.kt`; after `validate`/`waitFor`, `highlights("see @src/x.kt")` → `MENTION`.
- **Mention under caret is not flagged (new):** with `exists` empty, `highlights("@nope", caret = 5)`
is empty (skipped while being typed).
- **Update** `test highlights ignore untracked mentions`: keep asserting the *synchronous*
result is empty (pending), and add the async resolution assertion above. Rename to reflect
"pending → resolves to error".
- Existing tracked/`git-changes`/longest-first highlight tests keep passing unchanged.
### `PromptPanelTest.kt`
- **Undo/redo (new):** build `PromptPanel` with completion, `realize`, get
`field.getEditor(false)`. Insert text inside a `WriteCommandAction`/`CommandProcessor`
command, then resolve `PlatformCoreDataKeys.FILE_EDITOR` from the editor content
component's data context (`DataManager.getInstance().getDataContext(...)`), assert it is
non-null and wraps the prompt document, and that
`UndoManager.getInstance(project).undo(fileEditor)` reverts the text (and `redo` reapplies).
This exercises the real `uiDataSnapshot` contribution end-to-end.
- **Error highlight (new):** set `fileResolver`/`fileMatches` so `@missing` resolves to empty;
set `field.text = "@missing"`, pump events + `waitFor`, assert a highlighter spans
`@missing` with `CodeInsightColors.WRONG_REFERENCES_ATTRIBUTES`.
- **Update** `test prompt editor highlights validated commands and mentions` (currently
asserts `@unknown` has no highlight): make it deterministic under the new async validation —
either drive `@unknown` to resolve-not-found and assert the INVALID span, or keep it pending
and assert no synchronous highlight. Keep the `/new` + `@git-changes` assertions.
- Existing clear/bounded highlighter tests: confirm still ≤ 2 for `/new @git-changes`.
---
## Verification
From `packages/kilo-jetbrains/`:
- `./gradlew typecheck` (or `bun run typecheck`)
- `./gradlew test` (targeted: the two prompt test classes above)
Requires Java 21 (only diagnose Java if Gradle fails with a Java error). Also do a manual
`./gradlew runIde` smoke check: Cmd-Z/Cmd-Shift-Z in the prompt, mid-token completion, and a
not-found `@mention` turning red.
## Touched files
- `frontend/.../session/ui/editor/SessionEditorTextField.kt` — expose `FILE_EDITOR` (undo).
- `frontend/.../session/ui/prompt/KiloPromptCompletionProvider.kt` — token-end insert fix;
async existence validation + `INVALID` highlight kind; `scope` ctor param.
- `frontend/.../session/ui/prompt/PromptPanel.kt` — pass caret, drive validation, map
`INVALID` to error attributes.
- `frontend/.../session/SessionUi.kt` — pass `scope = cs` to the provider.
- `frontend/src/test/.../session/ui/prompt/KiloPromptCompletionProviderTest.kt` — tests.
- `frontend/src/test/.../session/ui/PromptPanelTest.kt` — tests.
- `frontend/src/test/.../testing/FakeWorkspaceRpcApi.kt` — `fileResolver` hook for tests.
@@ -1,214 +0,0 @@
# JetBrains prompt: syntax-highlight @mentions and /commands
## Goal
Visually distinguish, inside the JetBrains chat prompt editor, the two token kinds the
completion system already understands:
- `@mention` tokens (file/folder paths, plus the special `@terminal` / `@git-changes`)
- `/command` tokens (client slash actions + server commands), only at the very start of the prompt
Highlighting is **semantic / validated** (the chosen mode): only mentions that resolve to a
real tracked path or special mention, and only `/commands` that match a known command, get
colored. Bare `@foo` / `/foo` that aren't real are left as plain text — same rule the VS Code
webview uses (`buildHighlightSegments` only paints tracked paths).
## Current state (verified)
The completion feature is already implemented and committed (`3150da9986` +
`7fd80d0af9` + `0c2a7c1c5b`). Relevant pieces:
- `KiloPromptCompletionProvider` (`frontend/.../session/ui/prompt/KiloPromptCompletionProvider.kt`)
is a `TextCompletionProvider` that:
- owns the token grammar in `token(text, offset)` (`:158-165`): SLASH = leading `/` with the
text before it blank and no space; MENTION = `@` with no space.
- tracks inserted file-mention paths in a synchronized `paths` set; exposes `mentionPaths()`
(`:41`) and `clearMentions()` (`:43`).
- knows client action names via `clientNames()` (`:48`) and reads server commands from
`workspace.state.value.commands` (`:75-77`).
- inserts `@terminal` / `@git-changes` via `special()` (`:127-131`) — note these are **not**
added to `paths`.
- The editor is a real `EditorEx` behind `EditorTextField`:
`PromptPanel.editor` = `PromptEditorTextField(project, this, completion)` →
`SessionEditorTextField` (`PlainTextFileType`, completion-backed document).
The live editor is reachable via `editor.getEditor(false): EditorEx?` (lazy, non-null once shown).
- `PromptPanel` already has the hooks we need:
- a `DocumentListener.documentChanged` (`:222-229`) that runs on the EDT on every edit.
- `applyStyle(style)` (`:330-339`) re-applies the editor color scheme on theme change.
- `clear()` (`:342-348`) resets text + mentions.
- `completion: KiloPromptCompletionProvider?` field (`:91`) — null for the question
custom-answer editor, so highlighting naturally scopes to the prompt only.
- There is a proven in-repo highlighting precedent: `MdViewHybrid.applyTerm/applyShell`
(`ui/md/hybrid/MdViewHybrid.kt:641-690`) + `MdShellHighlight` (`ui/md/hybrid/MdShellHighlight.kt`)
apply ranges via `editor.markupModel.addRangeHighlighter(TextAttributesKey, start, end,
HighlighterLayer.SYNTAX + 1, HighlighterTargetArea.EXACT_RANGE)`.
There is **no** existing highlighting in the prompt editor today — confirmed no `markupModel`,
`RangeHighlighter`, or `TextAttributes` usage in `prompt/` or `editor/`.
## Options for how to highlight (with recommendation)
### Option A — MarkupModel range highlighters, recomputed on edit (RECOMMENDED)
Recompute the validated token ranges on each document change and apply them to the live
editor's `markupModel` with theme-aware `TextAttributesKey`s, mirroring `MdViewHybrid`.
- Pros: matches the existing in-repo pattern; entirely kilo-owned frontend code (no shared
files, no upstream merge surface, no extension-point registration); theme-aware; trivially
supports the **semantic** rule because the validation data (tracked paths, known commands)
is right there in the completion provider; recompute cost is negligible for short prompt text.
- Cons: highlights are decorative only (no PSI); we recompute the whole (small) buffer per edit.
### Option B — Custom lexer-based `EditorHighlighter` via `EditorEx.setHighlighter`
Write a small `Lexer` + `SyntaxHighlighter` for `@`/`/` tokens and set it on the editor in
`addSettingsProvider`. The platform then maintains incremental highlighting.
- Pros: platform-managed incremental highlighting; integrates with the editor color scheme.
- Cons: a hand-written `Lexer` is more code; crucially a lexer only sees **characters**, not the
tracked-paths set or the loaded command list, so it cannot do the **semantic / validated**
highlighting the user asked for (it would color any `@word`/`/word`). It also has to coexist
with the completion-backed document setup. Heavier for a worse fit.
### Option C — Full custom `Language` + `FileType` + `Annotator` (PSI)
Register a dedicated language with a parser and an `Annotator` for semantic coloring.
- Pros: the "proper" platform way; PSI unlocks future features.
- Cons: large effort and registration surface for two token kinds; conflicts with the existing
plain-text + completion document; overkill.
**Recommendation: Option A.** It is the smallest change, reuses a tested repo pattern, keeps
everything in kilo-owned files, and is the only option that cleanly expresses semantic
validation. The rest of this plan implements Option A.
## Design (Option A)
### 1. Compute highlight ranges in the completion provider
The provider already owns the grammar and the validation data, so the range computation belongs
there (single source of truth, no duplicated parsing).
Add to `KiloPromptCompletionProvider`:
```kotlin
enum class HighlightKind { MENTION, COMMAND }
data class Highlight(val start: Int, val end: Int, val kind: HighlightKind)
/** Validated highlight ranges for the whole prompt text. EDT-safe, pure over [text]. */
fun highlights(text: String): List<Highlight>
```
Rules inside `highlights(text)`:
- **Command** (at most one, at the start): if `text` starts with `/`, take the run up to the
first whitespace as `name`; if `name` is in `clientNames()` or in
`workspace.state.value.commands.map { it.name }`, emit `Highlight(0, 1 + name.length, COMMAND)`.
Mirrors the SLASH branch of `token()` and `SessionUi.serverCommand`.
- **Mentions** (anywhere): for each candidate set member — the tracked `mentionPaths()` plus the
literals `terminal` and `git-changes` — find every `@<value>` occurrence whose end is at a
whitespace or end-of-text, and emit `Highlight(idx, idx + 1 + value.length, MENTION)`. Match
**longest value first** so `@src/a.tsx` isn't shadowed by `@src/a.ts` (same precaution as
webview `syncMentionedPaths`/`findMentionRange`). Skip overlaps already covered.
This keeps `@terminal`/`@git-changes` highlighted even though they aren't in `paths`, by checking
the two literals explicitly.
### 2. Apply ranges to the live editor in `PromptPanel`
- Add a field to track our own highlighters so we never disturb anything else on the editor:
`private val highlighters = mutableListOf<RangeHighlighter>()`.
- Add `@RequiresEdt private fun syncHighlights()`:
- `val provider = completion ?: return` (no provider → question editor → no highlighting).
- `val ed = editor.getEditor(false) ?: return` (editor not realized yet).
- Remove our previous highlighters: `highlighters.forEach(ed.markupModel::removeHighlighter); highlighters.clear()`.
- For each `provider.highlights(ed.document.text)`, clamp to `document.textLength`, then
`ed.markupModel.addRangeHighlighter(keyFor(kind), start, end, HighlighterLayer.SYNTAX + 1,
HighlighterTargetArea.EXACT_RANGE)` and store the returned highlighter.
- `keyFor(MENTION)` / `keyFor(COMMAND)` map to `TextAttributesKey`s (see step 3).
- Call `syncHighlights()` from:
- the existing `documentChanged` listener (`:222-229`), after `triggerCompletion(e)` — covers
typing, completion inserts (which mutate the document and update `paths`), paste, and clear.
- the `addSettingsProvider` block (after the editor is configured) so the first realization of
the editor paints existing draft text.
- `applyStyle(...)` is not strictly required because `TextAttributesKey` highlighters recolor
with the scheme automatically and the editor instance is retained, but add a
`syncHighlights()` there too for safety on Look-and-Feel/scheme changes.
- when the server command list arrives: the `commands` list can load after the user has typed
`/foo`. Re-run `syncHighlights()` when workspace state updates. `PromptPanel` doesn't observe
workspace state directly today; the simplest wiring is to expose a `PromptPanel.refreshHighlights()`
(EDT) and call it from `SessionUi` where it already reacts to `SessionControllerEvent.WorkspaceReady`
(`SessionUi.kt:377`). Keep this a no-op-cheap call.
### 3. Colors — theme-aware `TextAttributesKey`
Per the JetBrains UI guidelines, prefer existing platform keys (as `MdShellHighlight` does with
`DefaultLanguageHighlighterColors.*`). Use:
- `MENTION` → `DefaultLanguageHighlighterColors.METADATA` (renders like `@annotation` accents —
a natural fit for `@mentions`).
- `COMMAND` → `DefaultLanguageHighlighterColors.KEYWORD` (distinct, keyword-like).
These require **no** registration and recolor with the active editor scheme automatically.
Optional enhancement (not required for first cut): register two custom keys
(`KILO_PROMPT_MENTION`, `KILO_PROMPT_COMMAND`) with the above as fallbacks plus a color-settings
page, so users can recolor them. Defer unless we want user-configurable colors.
### 4. Edge cases
- Token at caret while typing: highlight only validated tokens, so a half-typed `@de` stays plain
until completion inserts a real path (which adds it to `paths` and triggers a recompute). This
matches the webview's tracked-path behavior and avoids flicker.
- Overlapping/duplicate paths: longest-first matching + skip-covered avoids double highlighting.
- Empty/`null` editor: guard with `getEditor(false)?` (already the pattern used at `:335`,`:467`).
- Performance: prompt text is short; full rescan per keystroke is fine. No debounce needed; if it
ever matters, gate on "text contains '@' or starts with '/'".
## Files to change (all kilo-owned — no shared/upstream files, no `kilocode_change` markers needed)
- `frontend/.../session/ui/prompt/KiloPromptCompletionProvider.kt`
- add `HighlightKind`, `Highlight`, and `highlights(text)`.
- `frontend/.../session/ui/prompt/PromptPanel.kt`
- add `highlighters` list, `syncHighlights()`, call sites (document listener,
`addSettingsProvider`, `applyStyle`), and a public `refreshHighlights()` for the
command-loaded case.
- `frontend/.../session/SessionUi.kt`
- call `prompt.refreshHighlights()` from the `WorkspaceReady` handler (`:377`).
No new bundle strings, RPC, DTOs, backend, or extension-point registration.
## Testing
Follow the existing JetBrains pattern (`BasePlatformTestCase`, no mocks). There is already
`KiloPromptCompletionProviderTest` using `myFixture.configureByText` + `TextCompletionUtil.installProvider`.
1. Provider unit tests for `highlights(text)` (pure function, no editor needed):
- `/new ...` at start with `new` a known client action → one COMMAND range over `/new`.
- `/bogus ...` → no COMMAND range.
- `/foo` not at start (e.g. `hi /foo`) → no range.
- `@src/a.ts` present in `paths` → MENTION range; `@src/a.tsx` longest-first not shadowed.
- `@terminal` / `@git-changes` literals → MENTION ranges without being in `paths`.
- `@unknownPath` not tracked → no range.
2. `PromptPanelTest` (real EDT) — type text, drain EDT, assert the editor's markup model contains
the expected number of highlighters with the expected ranges/keys; assert `clear()` removes
them; assert no highlighter growth across repeated edits (bounded count), per the
stress/leak-test guidance for editor-backed UI.
## Verification
From `packages/kilo-jetbrains/` (requires Java 21 — check `java -version` first):
- `./gradlew typecheck`
- `./gradlew test` (or the smallest relevant test task for the frontend module)
Optionally `./gradlew runIde` to eyeball mention/command coloring in the prompt.
## Out of scope / notes
- Completion, file search, `@terminal`/`@git-changes` attachments, server-command execution, and
send-time file parts already exist — this plan only adds visual highlighting on top.
- `@terminal` content is currently a backend stub (`KiloWorkspaceRpcApiImpl.terminalOutput` returns
`null`) and `searchFiles` never advertises `terminal=true`; that's a separate completion-content
gap, independent of highlighting. The literal `@terminal` will still be highlighted if typed.
- Keep all logic in the kilo-owned `frontend` files above to preserve the small upstream diff.
@@ -1,65 +0,0 @@
# JetBrains Root Mention Completions
## Goal
When a JetBrains user types `@` in the prompt with no query, show useful file/folder suggestions from the workspace root instead of only `@git-changes`, matching the VS Code behavior more closely.
## Current Findings
- Frontend completion is implemented in `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProvider.kt`.
- `mention(prefix, result)` calls `search(prefix)` for all mention completions. With plain `@`, `prefix == ""`.
- The backend RPC is `KiloWorkspaceRpcApiImpl.searchFiles(...)` in `packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceRpcApiImpl.kt`.
- Backend `search(project, base, query, limit)` currently returns `emptyList()` when `query.trim().isBlank()`, so the frontend only has special mentions such as `@git-changes`.
- VS Code file mentions request both file and folder search and merge file/folder/open-editor results in `packages/kilo-vscode/src/kilo-provider/file-search.ts`; for the JetBrains fix, the smallest useful parity step is root-directory prepopulation for blank queries.
## Implementation Plan
1. Keep the existing fuzzy search path unchanged for non-empty mention queries.
- Leave the current `GotoFileModel` / `ChooseByName*` code in `KiloWorkspaceRpcApiImpl.search(...)` intact for typed queries such as `@main` or `@src/foo`.
- Only branch when `query.trim().isBlank()`.
2. Add a blank-query root listing path in the backend.
- In `KiloWorkspaceRpcApiImpl.search(...)`, replace `if (text.isBlank()) return emptyList()` with a call to a new private helper, for example `roots(project, base, limit)`.
- Resolve `base` through `LocalFileSystem.getInstance().refreshAndFindFileByNioFile(base)`.
- Read immediate children only, not a recursive tree, to keep `@` completion fast and predictable.
- Convert each child through existing `fileDto(base, vf)` so returned paths remain relative and normalized the same way as fuzzy results.
3. Filter root entries to avoid noisy or invalid suggestions.
- Exclude `.git` explicitly.
- Use IntelliJ project indexing APIs such as `ProjectFileIndex.getInstance(project)` to skip excluded files/directories when available.
- Preserve directories by returning `WorkspaceFileDto(..., directory = true)` via `fileDto` so the existing frontend folder icon path continues to work.
4. Sort root suggestions for a useful default list.
- Put directories before files, matching common file picker behavior.
- Sort each group case-insensitively by name.
- Respect the existing `limit` after sorting.
- Do not pin or special-case repository-specific names unless later UX feedback asks for it.
5. Keep special mention behavior unchanged.
- `@git-changes` should still appear when `search.git` is true and the prefix matches, including blank prefix.
- The frontend should continue to add special mentions before file suggestions, so git changes remains at the top when available.
6. Add focused frontend completion coverage.
- Update `KiloPromptCompletionProviderTest` with a test for `@<caret>` where the fake RPC returns root entries plus `git = true`.
- Assert the lookup contains `git-changes` and returned file/folder paths.
- Assert the RPC saw a blank query (`listOf("")`) so the behavior is explicitly covered.
- If needed, extend the existing `file(...)` helper to create directory DTOs for icon/type coverage, but keep the test focused on visible completion values.
7. Add backend coverage if practical in the existing test harness.
- First look for an existing IntelliJ project fixture pattern in backend tests.
- If lightweight backend fixture setup is practical, add a test around `searchFiles(directory, "", limit)` that creates root children and verifies immediate relative entries only.
- If backend fixture setup is heavy or brittle, rely on the frontend completion test plus `typecheck`, and note the backend behavior is exercised manually in sandbox.
8. Verification.
- Run the smallest relevant JetBrains checks from `packages/kilo-jetbrains/`.
- Start with the targeted frontend test class, e.g. `./gradlew :frontend:test --tests ai.kilocode.client.session.ui.prompt.KiloPromptCompletionProviderTest` if the module task supports it.
- Run `./gradlew typecheck` from `packages/kilo-jetbrains/` after tests pass.
- Do not run Java preflight unless Gradle reports a Java version or missing-Java failure.
## Non-Goals
- Do not change VS Code mention behavior.
- Do not introduce recursive blank-query search; initial `@` should show root-level entries only.
- Do not change how selected file mentions are serialized into prompt parts.
- Do not change rendering of mentioned files or synthetic file content; that is covered by the separate `jetbrains-file-mention-parity.md` plan.
- Do not modify shared OpenCode files; this work stays inside the Kilo JetBrains package.
-63
View File
@@ -1,63 +0,0 @@
# JetBrains Slash Actions Organization Plan
## Goal
Organize JetBrains built-in slash actions so names and metadata are centralized instead of scattered across `SessionUi` and tests.
## Current Findings
- Built-in client slash actions are currently defined inline in `frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt` inside `slashActions()`.
- `SlashAction` is currently a nested data class inside `KiloPromptCompletionProvider`.
- Tests instantiate `KiloPromptCompletionProvider.SlashAction("new", ...)` directly in:
- `frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProviderTest.kt`
- `frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt`
- I found no `git-review` literal in `packages/kilo-jetbrains`. The prompt package does contain hardcoded `git-changes`, but that is an `@mention` resource, not a slash action.
## Implementation Plan
1. Add a top-level `SlashAction` class in the prompt package:
- Path: `frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/SlashAction.kt`
- Package: `ai.kilocode.client.session.ui.prompt`
- Move the current data shape out of `KiloPromptCompletionProvider`.
2. Model built-in actions as named specs inside `SlashAction`:
- Add an inner `Spec` data class or equivalent to hold `name`, `descriptionKey`, and `hints`.
- Add named constants for each built-in action:
- `NEW`
- `SESSIONS`
- `MODELS`
- `AGENTS`
- `VARIANT`
- `COMPACT`
- `SETTINGS`
- `HELP`
- Add an ordered `ALL` list containing all specs.
3. Keep execution binding in `SessionUi`:
- Replace inline string literals like `"new"`, `"sessions"`, etc. with the named `SlashAction` specs.
- Build executable `SlashAction` instances by binding each spec to its handler lambda.
- Keep the current behavior and ordering unchanged.
4. Update `KiloPromptCompletionProvider` to use the top-level class:
- Remove the nested `data class SlashAction`.
- Constructor remains `actions: List<SlashAction>`.
- Existing provider logic can continue to use `action.name`, `action.description`, `action.hints`, and `action.action`.
5. Update tests to refer to centralized names:
- Replace `KiloPromptCompletionProvider.SlashAction` with `SlashAction`.
- Replace built-in hardcoded `"new"` references with `SlashAction.NEW.name` or helper-bound `SlashAction.NEW` where practical.
- Leave synthetic non-built-in test commands like `"next"` as custom test actions unless a test-only helper is clearer.
6. Keep localization unchanged:
- Continue using existing `prompt.slash.*` keys in `KiloBundle.properties`.
- `SlashAction` specs can store the message key; `SessionUi` can resolve descriptions when binding specs.
7. Verification:
- Run JetBrains typecheck from `packages/kilo-jetbrains`: `./gradlew typecheck`.
- Run targeted frontend tests if available through Gradle filtering for `KiloPromptCompletionProviderTest` and `PromptPanelTest`; otherwise run the smallest JetBrains test task that covers frontend tests.
## Non-Goals
- Do not change server/config command handling through `CommandDto`.
- Do not change `@git-changes` mention behavior unless explicitly requested; it is not a slash action.
- Do not add new built-in slash actions or change aliases/descriptions.
-75
View File
@@ -1,75 +0,0 @@
# Add JetBrains Slash Command Aliases
## Goal
Add the VS Code slash-command alias parity that JetBrains is missing, and make those aliases execute the same client-side actions when typed and sent directly.
## Current Findings
- VS Code aliases from `packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts`:
- `/clear` -> `/new`
- `/continue`, `/history`, `/resume` -> `/sessions`
- `/variants`, `/reasoning`, `/thinking` -> `/variant`
- `/smol`, `/condense` -> `/compact`
- JetBrains aliases currently in `SlashAction.kt`:
- `/history`, `/resume` -> `/sessions`
- `/modes` -> `/agents`
- `/reasoning` -> `/variant`
- `/smol` -> `/compact`
- Missing JetBrains aliases:
- `/clear`
- `/continue`
- `/variants`
- `/thinking`
- `/condense`
- `KiloPromptCompletionProvider` already uses `hints` as lookup strings and search matches, so adding missing hints improves autocomplete discovery.
- Direct send handling currently uses `serverCommand(text)` and only excludes canonical client names via `clientNames()`. It does not resolve client aliases, so manually sending `/clear` would fall through to a normal prompt unless selected from autocomplete.
## Implementation Plan
1. Update `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/SlashAction.kt`:
- Add `clear` to `NEW.hints`.
- Add `continue` to `SESSIONS.hints` while preserving `history` and `resume`.
- Add `variants` and `thinking` to `VARIANT.hints` while preserving `reasoning`.
- Add `condense` to `COMPACT.hints` while preserving `smol`.
2. Update `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProvider.kt`:
- Add a small client-action resolver, for example `fun clientAction(text: String): SlashAction?`.
- Parse only leading slash commands the same way `serverCommand(text)` does.
- Match the parsed name against each action's canonical `name` and `hints`.
- Return `null` for blank names and non-leading slash text.
- Update `clientNames()` or add a separate `clientTokens()` helper so `serverCommand(text)` also treats aliases as client-owned and does not route an alias to a server command with the same name.
- Update `highlights(text)` to highlight aliases as valid commands, not just canonical client command names.
3. Update `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt`:
- In `sendPrompt`, after clearing the prompt and before server-command dispatch, call the new client-action resolver.
- If a client action is found, execute `action.action()` and return without calling `controller.prompt(...)` or `controller.command(...)`.
- Keep scroll follow behavior consistent with the current send flow.
- This will also make directly typed canonical commands such as `/new` execute client actions, which aligns with the alias behavior and avoids sending client commands as ordinary prompts.
4. Add or update tests in `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProviderTest.kt`:
- Construct test actions with representative hints, including `clear` and `continue`.
- Assert slash completion finds a command by alias, e.g. `/cle<caret>` shows/selects `new`.
- Assert `clientAction("/clear")` resolves to the `new` action.
- Assert `clientAction("/continue")` resolves to the `sessions` action.
- Assert `clientAction("hi /clear")` is `null`.
- Assert `serverCommand("/clear")` is `null` even if a server command named `clear` exists.
- Assert `highlights("/clear")` marks the alias as `COMMAND`.
5. Add a focused `SessionUi` behavior test if an existing harness makes it straightforward:
- Verify sending `/clear` triggers `manager.newSession()` and does not call `controller.prompt`.
- If the harness would require heavy UI plumbing, rely on provider resolver tests plus typecheck and avoid brittle UI tests.
6. Add a changeset:
- Package: `@kilocode/kilo-jetbrains`
- Bump: `patch`
- Suggested text: `Support VS Code slash-command aliases in the JetBrains prompt.`
## Verification
Run the smallest relevant checks from `packages/kilo-jetbrains/`:
- `./gradlew test --tests ai.kilocode.client.session.ui.prompt.KiloPromptCompletionProviderTest`
- `bun run typecheck`
If the focused Gradle test filter is not accepted by the repo setup, run `./gradlew test` from `packages/kilo-jetbrains/` instead.
## Non-Goals
- Do not add `/export` or `/remote` in this change.
- Do not change server-side CLI slash commands.
- Do not introduce new RPC, backend services, or UI surfaces.
@@ -1,363 +0,0 @@
# JetBrains: slash commands + file mentions via native IntelliJ completion
## Goal
Reach VS Code parity for the prompt input in the JetBrains plugin:
- `/` slash commands (built-in client actions **and** server/custom commands a.k.a. workflows)
- `@` file/folder mentions
Render both with **IntelliJ's native completion lookup popup** (not a custom Swing popup), since the
prompt is already a real `EditorTextField` over `PlainTextFileType`.
File-mention search must use **JetBrains' own index API** (`FilenameIndex`). When the project index is
not built yet (dumb mode), the **mention completion only** must show a clear "indexing" message — the
rest of the plugin (including slash commands) must not wait or be blocked.
## Key facts established during research
- Prompt input: `session/ui/prompt/PromptPanel.kt` → `PromptEditorTextField` → `SessionEditorTextField : EditorTextField(project, PlainTextFileType.INSTANCE)`. No slash/mention handling today.
- Platform registers `TextCompletionContributor` for `language="TEXT"` (`LangExtensions.xml:1243`). It is a **no-op unless** a `TextCompletionProvider` is installed on that specific document's PSI file (`TextCompletionUtil.COMPLETING_TEXT_FIELD_KEY`). So enabling completion on the Kilo prompt does **not** affect other plain-text editors.
- Install path: either subclass `TextFieldWithCompletion` (which wires the provider + autopopup via `DocumentWithCompletionCreator`), or call `TextCompletionUtil.installProvider(psiFile, provider, autoPopup=true)` on the prompt document's PSI file.
- Autopopup: `AutoPopupController.getInstance(project).scheduleAutoPopup(editor)` (public, `analysis-impl`). For invocationCount 0 the contributor only fills when `AUTO_POPUP_KEY=true`.
- `TextCompletionUtil.getProvider` calls `DumbService.isUsableInCurrentContext(provider)`: a provider that is **not** `DumbAware` is skipped during indexing → no completion at all. Make the provider `DumbAware` so slash commands keep working while indexing.
- Slash-command data already exists: `KiloWorkspaceStateDto.commands: List<CommandDto>`, exposed via `Workspace.state` / `KiloWorkspaceService`.
- File search: existing `KiloWorkspaceRpcApi.files(directory, path)` is a **literal path resolver**, not fuzzy search. Need a new fuzzy-search RPC.
- Split-mode constraint: completion runs in the **frontend** process which has no project index; `FilenameIndex` lives on the **backend**. File search therefore must go over RPC, and the dumb-mode flag is a **backend** condition surfaced in the RPC result.
- Server commands run via CLI `POST /session/{id}/command` (SDK `session.command`). JetBrains has no command-execution path yet (`KiloSessionRpcApi` has `prompt` but no `command`).
- Prompt parts: `PromptPartDto(type, text?, mime?, url?, filename?)`. File mention → `type="file", url="file://abs"` (reference, not base64; base64 is only used for pasted media in `PromptAttachment.part()`).
- `DumbService.isDumb` (`platform/core-api/.../DumbService.kt:66`), `FilenameIndex.processAllFileNames(...)` + `FilenameIndex.getVirtualFilesByName(name, scope)`, `NameUtil.buildMatcher("*"+query)` → `MinusculeMatcher`.
- Backend project resolution helper already exists: `KiloWorkspaceRpcApiImpl.project(path: Path): Project?` and `file(...)`/`clean(...)`.
## Architecture decision
One frontend `TextCompletionProvider` handles **both** `/` and `@`. Slash data is local (workspace
state). Mention data comes from a new backend `searchFiles` RPC backed by `FilenameIndex`, with an
`indexing` flag. The provider is `DumbAware` so it always runs; mention branch renders an indexing
notice when the backend reports the index isn't ready. Slash never depends on the index.
```
PromptPanel editor (frontend, PlainText)
└─ DocumentListener: on '/' or '@' typed → AutoPopupController.scheduleAutoPopup(editor)
└─ TextCompletionContributor (platform, language=TEXT)
└─ KiloPromptCompletionProvider (DumbAware)
├─ getPrefix → detect SLASH vs MENTION vs none(null)
├─ SLASH → client actions + Workspace.state.commands (no index)
└─ MENTION → KiloWorkspaceService.searchFiles(dir,q) ──RPC──▶ backend
FileSearchResultDto(indexing, files)
indexing=true → show "Indexing… file mentions soon"
else → file/folder lookup elements
```
---
## Phase 0 — Wire native completion into the prompt editor
Files: `session/ui/editor/SessionEditorTextField.kt`, `session/ui/prompt/PromptEditorTextField.kt`,
`session/ui/prompt/PromptPanel.kt`, `session/SessionUi.kt`.
1. Add a `TextCompletionProvider?` parameter to `SessionEditorTextField` (default null so the
question custom-answer editor stays plain). When non-null, install it on the document's PSI file.
Prefer the documented path: build the document via
`LanguageTextField.createDocument(value, PlainTextLanguage.INSTANCE, project, TextCompletionUtil.DocumentWithCompletionCreator(provider, autoPopup=true))` and pass it to the `EditorTextField` constructor; or call `TextCompletionUtil.installProvider(PsiDocumentManager.getInstance(project).getPsiFile(document), provider, true)` after construction. Keep the existing `uiDataSnapshot`/`PromptDataKeys.SEND` behavior.
2. `PromptEditorTextField` forwards the provider to `SessionEditorTextField`.
3. `PromptPanel` constructor gains a `completion: TextCompletionProvider` param (built in `SessionUi`,
see Phase 1/2). Keep all existing `addSettingsProvider` styling/soft-wrap/height logic — it still
applies because `TextFieldWithCompletion`/`EditorTextField` is the same component family.
4. In `PromptPanel`'s existing `DocumentListener.documentChanged`, after a single-char insertion of
`/` (only when it is the first non-space char of the document) or `@`, call
`AutoPopupController.getInstance(project).scheduleAutoPopup(editorComponent)`. Guard with
`project.isDisposed`.
5. `SessionUi.kt:328` constructs `PromptPanel` — pass the provider here, where `project`, `workspace`,
`controller`, and `app` are in scope.
Acceptance: typing `/` or `@` in the prompt opens the native lookup; other plain-text editors are
unaffected.
---
## Phase 1 — Slash commands
New file: `session/ui/prompt/PromptSlashCompletion.kt` (helper that produces `LookupElement`s for the
slash branch), consumed by the shared provider in Phase 2's `fillCompletionVariants`.
### 1a. Client-action commands (mirror `useSlashCommand.ts`)
Define a small list of built-ins, each: name, description, hints (aliases), and an `action: () -> Unit`.
Map to existing JetBrains capabilities:
| Command | Action target (existing) |
|---|---|
| `new` | start new session (SessionManager / new-session flow) |
| `sessions` (`history`,`resume`) | open history (`HistoryPanel`/`SessionManager`) |
| `models` | open `PromptPanel.model` (`ModelPicker`) |
| `agents` (`modes`) | open `PromptPanel.mode` (`ModePicker`) |
| `variant` (`reasoning`) | open `PromptPanel.reasoning` (`ReasoningPicker`) |
| `compact` (`smol`) | `controller.compact(...)` |
| `settings` | open Kilo settings |
| `help` | open `https://kilo.ai/docs` |
- Each picker likely needs a public `open()`/`showPopup()` hook if it doesn't already expose one; add a
minimal method on the picker rather than reaching into internals.
- `export`/`remote` from VS Code are **dropped** for JetBrains (no equivalent capability today). Do not
invent features.
- Lookup element: `LookupElementBuilder.create(name).withPresentableText("/"+name)
.withTailText(" "+description, true).withLookupStrings(hints)
.withIcon(...)`. `InsertHandler`: clear the document (`context.document.setText("")`) and run
`action()` on the EDT via `ApplicationManager.getApplication().invokeLater`. Mark with a badge tail
("custom"/"skill"/"mcp") analogous to VS Code where source info is available.
### 1b. Server/custom commands (workflows)
- Source: `workspace.state.value.commands` (`List<CommandDto>` already populated). Filter out names
that collide with client actions. Build lookup elements; `InsertHandler` inserts `"/"+name+" "` and
positions the caret after it (do **not** clear / run an action).
- Execution at submit time (mirrors VS Code `prompt-input-utils` + `sendCommand`):
- In `PromptPanel.submit`/`SessionUi.sendPrompt`, detect a leading `/<name>` where `<name>` is a known
server command; split into `name` + `args` (rest of the text). If matched, route to a new
controller command path instead of a normal prompt.
- Add `SessionController.command(name: String, args: String, files: List<PromptPartDto>)` mirroring
`prompt(...)`: lazily create session if needed, then call a new session RPC.
- New RPC `KiloSessionRpcApi.command(id, directory, name, args, model: ModelSelectionDto?)` →
backend `KiloSessionRpcApiImpl.command(...)` → CLI `POST /session/{id}/command`
(mirror `chat.prompt`; reuse the existing CLI HTTP client used by chat). Add a `CommandDto`-free
request payload as needed.
- Optimistic display + event flow reuse the existing prompt pipeline.
Acceptance: `/` shows built-ins + workflows; selecting a built-in runs its action and clears input;
selecting a workflow inserts `/name `; submitting `/name args` runs the workflow.
---
## Phase 2 — File mentions via `FilenameIndex`, with dumb-mode handling
### 2a. Shared DTO + RPC
File: `shared/.../rpc/dto/WorkspaceFileDto.kt` (add a result wrapper) or a new
`FileSearchResultDto.kt`:
```kotlin
@Serializable
data class FileSearchResultDto(
val indexing: Boolean = false,
val files: List<WorkspaceFileDto> = emptyList(),
val terminal: Boolean = false, // an active terminal exists → offer @terminal
val git: Boolean = false, // project under git → offer @git-changes
)
```
File: `shared/.../rpc/KiloWorkspaceRpcApi.kt` — add:
```kotlin
/** Fuzzy file/folder search via the IDE index. indexing=true means the index isn't ready yet. */
suspend fun searchFiles(directory: String, query: String, limit: Int = 50): FileSearchResultDto
```
### 2b. Backend impl (JetBrains API)
File: `backend/.../rpc/KiloWorkspaceRpcApiImpl.kt` — add `searchFiles`:
```kotlin
override suspend fun searchFiles(directory: String, query: String, limit: Int): FileSearchResultDto {
val base = file(clean(directory) ?: directory) ?: return FileSearchResultDto()
val project = project(base) ?: return FileSearchResultDto()
if (DumbService.getInstance(project).isDumb) return FileSearchResultDto(indexing = true)
return try {
readAction { search(project, base, query, limit) } // com.intellij.openapi.application.readAction
} catch (e: IndexNotReadyException) {
// indexing started mid-search — surface the same not-ready state, do not block
FileSearchResultDto(indexing = true)
}
}
```
`search(project, base, query, limit)` (read action):
- `val matcher = NameUtil.buildMatcher("*").build()` when query blank, else `NameUtil.buildMatcher("*$query").build()` (`MinusculeMatcher`, case-insensitive substring/camel match).
- Collect candidate names: `FilenameIndex.processAllFileNames({ name -> if (matcher.matches(name)) names.add(name); names.size < CAP }, GlobalSearchScope.projectScope(project), null)` (cap candidate names, e.g. 2000, to bound work).
- For each matched name: `FilenameIndex.getVirtualFilesByName(name, GlobalSearchScope.projectScope(project))` → for each `vf`, compute path relative to `base` (skip if not under `base`), build `WorkspaceFileDto(path = relative, name = vf.name, directory = vf.isDirectory)`.
- Sort by `matcher.matchingDegree(name)` desc, then shorter path; take `limit`.
- Return `FileSearchResultDto(indexing = false, files = …)`.
Notes:
- Do **not** call `DumbService.waitForSmartMode` / `runReadActionInSmartMode` — those block. We explicitly want the non-blocking `indexing=true` short-circuit.
- Perf escape hatch (only if `processAllFileNames` proves too heavy on huge projects): switch to
`GotoFileModel` / `ChooseByNameModelEx` which the IDE's "Go to File" uses. Keep `FilenameIndex` as the
default; note this alternative in code comments only if needed.
- Imports: `com.intellij.psi.search.FilenameIndex`, `com.intellij.psi.search.GlobalSearchScope`,
`com.intellij.openapi.project.DumbService`, `com.intellij.openapi.project.IndexNotReadyException`,
`com.intellij.psi.codeStyle.NameUtil`, `com.intellij.openapi.application.readAction`.
### 2c. Frontend service
File: `frontend/.../app/KiloWorkspaceService.kt` — add:
```kotlin
suspend fun searchFiles(directory: String, query: String, limit: Int = 50): FileSearchResultDto =
try { call { searchFiles(directory, query, limit) } }
catch (e: Exception) { LOG.warn("file search failed dir=$directory q=$query", e); FileSearchResultDto() }
```
### 2d. Mention branch in the provider
In `fillCompletionVariants` MENTION branch (runs on the completion **background** thread, off EDT):
- `val result = runBlockingCancellable { service.searchFiles(directory, prefix, 50) }`
(`com.intellij.openapi.progress.runBlockingCancellable`). Background-thread blocking is acceptable here;
never do this on the EDT.
- If `result.indexing`:
- `resultSet.addLookupAdvertisement(KiloBundle.message("prompt.mention.indexing"))`
- add one non-inserting info element:
`LookupElementBuilder.create("").withPresentableText(message).withIcon(AllIcons.General.Information)`
with an `InsertHandler` that no-ops (or removes itself). Optionally
`resultSet.restartCompletionWhenNothingMatches()` and `DumbService.getInstance(project).runWhenSmartMode { scheduleAutoPopup }` (frontend project) so the popup refreshes once indexing finishes.
- Else build a `LookupElement` per file/folder: present text `@`+path, tail = parent dir, file-type icon
(`FileTypeManager`/`AllIcons.Nodes.Folder`), `withLookupString(path)`; `InsertHandler` replaces the
`@query` token with `@path ` (see Phase 3).
Directory used for `searchFiles` = `workspace.directory` (available in `SessionUi`); pass it into the
provider at construction.
Acceptance: `@` shows fuzzy file/folder matches from the IDE index; during initial indexing the
mention popup shows an "indexing" notice while slash commands and the rest of the UI work normally.
### 2e. Special mentions: `@terminal` and `@git-changes`
Mirror VS Code (`terminal-context-utils.ts`, `git-changes-context-utils.ts`). Each is a single
synthetic mention that, on submit, embeds gathered text as a data-URL `file` part:
`PromptPartDto(type="file", mime="text/plain", url="data:text/plain;charset=utf-8,<encoded>", filename=...)`
(`terminal-output.txt` / `git-changes.txt`). The `@terminal` / `@git-changes` tokens stay in the text.
Completion candidates (in the MENTION branch, **before** file results, filtered by prefix like VS Code's
`getTerminalMentionResult`/`getGitChangesMentionResult`):
- `@terminal` — show only when a terminal is available (terminal tool window / `org.jetbrains.plugins.terminal` present and has a session).
- `@git-changes` — show only when the project root is under VCS/git.
These two require **no index**, so offer them even while `indexing=true` (do not gate behind dumb mode).
Backend content RPCs (new on `KiloWorkspaceRpcApi`, since content depends on IDE/project state):
- `suspend fun terminalOutput(directory: String): String?` — read the active terminal widget scrollback.
Use `org.jetbrains.plugins.terminal.TerminalView.getInstance(project)` → active
`ShellTerminalWidget`/content; best-effort `getText()` of the terminal buffer. Return `null` if the
terminal plugin/tool window is unavailable so the frontend can hide the mention. Guard the
terminal-plugin dependency so the plugin still loads if it's absent.
- `suspend fun gitChanges(directory: String): String?` — current uncommitted changes as a unified diff.
Prefer VCS API (`ChangeListManager.getInstance(project).allChanges` → render patch) or shell
`git diff` (+ `git diff --staged`) via the existing CLI/process util scoped to `directory`. Return
`null`/empty when not a git repo.
Availability flags for the completion popup: expose lightweight checks
(`terminalAvailable(directory)` / `gitAvailable(directory)`) — either as separate RPCs or folded into
`FileSearchResultDto` (e.g. add `terminal: Boolean`, `git: Boolean` flags returned by `searchFiles`).
Prefer folding into `searchFiles` to avoid extra round-trips; the provider then knows which special
mentions to offer alongside file results (and during indexing, `searchFiles` can still return the flags
with `indexing=true` and empty `files`).
Submit-time assembly (Phase 3): when `@terminal` / `@git-changes` is present in the text, call the
corresponding backend RPC, encode the returned text, and append the data-URL `file` part. Skip if the
RPC returns null/empty.
Acceptance: `@` offers `@terminal` (when a terminal exists) and `@git-changes` (when under git); both are
offered even while indexing; submitting attaches the gathered terminal/diff text as a file part.
---
## Phase 3 — Mention insert + attachment building on submit
- Track inserted mention paths. Simplest robust approach: on submit, re-derive mentions from the text
by scanning for `@<path>` tokens whose `<path>` matches a path we offered/inserted. Mirror VS Code's
`buildFileAttachments`: keep a `Set<String>` of inserted paths in the provider/panel; the mention
`InsertHandler` adds the path to that set.
- `InsertHandler` for a file: compute the `@query` token range before the caret, replace with
`@<path> ` (trailing space unless next char is whitespace), and register `<path>`.
- In `PromptPanel.submit`, for each tracked path still present as `@<path>` in the text, append a
`PromptPartDto(type="file", mime="text/plain", url="file://<abs>", filename=<name>)` where
`<abs> = directory + "/" + path` (or the path if already absolute). Keep the `@path` text in the
message (matches VS Code).
- If the text contains `@terminal` / `@git-changes`, call `workspace.terminalOutput(dir)` /
`workspace.gitChanges(dir)`; when non-empty, append a data-URL `file` part (Phase 2e). These RPCs run
off the EDT inside the existing pooled-thread submit path (`PromptPanel.submit` already does
`executeOnPooledThread`).
- Optional polish (defer): atomic backspace deletion of a whole `@path`, arrow-key skip, selection
snapping — the editor already handles most caret behavior; only add if needed.
Acceptance: submitting a prompt with `@src/Foo.kt` sends a `file` part referencing that file plus the
text.
---
## i18n / resources
- Add bundle keys: `prompt.mention.indexing`, slash command descriptions, badges
(`prompt.slash.badge.custom/skill/mcp`) in the frontend `*.properties` (mirror VS Code `en.ts`
keys). Keep all user-visible strings in properties.
## Testing (extend `SessionControllerTestBase` / `BasePlatformTestCase`)
- `KiloPromptCompletionProvider` prefix detection: SLASH only at text start, MENTION after whitespace,
null otherwise (pure unit test on `getPrefix`).
- Slash: client actions present + invoke their action and clear text; server commands inserted as
`/name `; submit routes `/name args` to `controller.command`.
- Mentions: with a real indexed test project, `searchFiles` returns expected relative paths; build
lookup elements; insert handler produces `@path ` and a `file` part on submit.
- **Dumb-mode**: drive `searchFiles` while `DumbService` is dumb (or stub the backend RPC to return
`indexing=true` via `FakeSessionRpcApi`-style fake for the workspace RPC) and assert the mention
branch yields the indexing advertisement/info element and **no** file elements, while the slash
branch still returns commands. Use `DumbModeTestUtils` / `BasePlatformTestCase` dumb helpers for the
backend search unit test.
- Native completion smoke test: install provider on an `EditorTextField`, type `/` and `@`, assert the
platform builds the expected lookup items (use `CompletionAutoPopupTestCase`/`myFixture` patterns where
feasible; otherwise test the provider directly via a synthetic `CompletionParameters`).
- No new EDT/threading violations; `searchFiles` runs in a read action; RPC never called on EDT.
## File-change checklist
Shared:
- `shared/.../rpc/dto/` add `FileSearchResultDto` (with `indexing`/`terminal`/`git` flags).
- `shared/.../rpc/KiloWorkspaceRpcApi.kt` add `searchFiles`, `terminalOutput`, `gitChanges`.
- `shared/.../rpc/KiloSessionRpcApi.kt` add `command` (Phase 1b).
Backend:
- `backend/.../rpc/KiloWorkspaceRpcApiImpl.kt` add `searchFiles` (+ `search` helper), `terminalOutput`
(terminal plugin, guarded), `gitChanges` (VCS API or `git diff`), imports.
- `backend/.../rpc/KiloSessionRpcApiImpl.kt` add `command` → CLI `/session/{id}/command` (Phase 1b).
- `backend/build.gradle.kts`: add an **optional** dependency on `org.jetbrains.plugins.terminal` for
terminal scrollback; keep it soft so the plugin loads without it (reflective/`PluginManager` guard or
`<depends optional>` style). Confirm against AGENTS "bundle third-party libs" rule — platform plugin
deps are declared via the IntelliJ Platform Gradle plugin, not bundled.
Frontend:
- `frontend/.../app/KiloWorkspaceService.kt` add `searchFiles`, `terminalOutput`, `gitChanges`.
- `frontend/.../session/ui/editor/SessionEditorTextField.kt` accept + install provider.
- `frontend/.../session/ui/prompt/PromptEditorTextField.kt` forward provider.
- `frontend/.../session/ui/prompt/PromptPanel.kt` accept provider param; autopopup trigger in doc
listener; mention→file-part assembly in `submit`; server-command submit routing.
- `frontend/.../session/ui/prompt/KiloPromptCompletionProvider.kt` (new): `TextCompletionProvider, DumbAware`.
- `frontend/.../session/ui/prompt/PromptSlashCompletion.kt` (new, optional split): slash lookup builders.
- `frontend/.../session/SessionUi.kt` construct provider with `project`/`workspace`/`controller`/`app`
and pass to `PromptPanel`; wire client-action targets (picker `open()` hooks).
- `frontend/.../session/controller/SessionController.kt` add `command(name,args,files)` (Phase 1b).
- Picker components (`ModePicker`/`ModelPicker`/`ReasoningPicker`) expose a public `open()` if missing.
- Frontend `*.properties` add bundle keys.
## Scope decisions (confirmed)
1. **Server-command execution (Phase 1b): included now** — new `KiloSessionRpcApi.command` + CLI
`/session/{id}/command` wiring is part of this plan.
2. **Special mentions: include `@terminal` + `@git-changes`** (Phase 2e), gated on availability.
3. **`/export` and `/remote`: dropped** on JetBrains (no equivalent capability).
## Suggested execution order
Phase 0 → 1a (client actions) → 2a–2d (files + dumb-mode) → 3 (file attachments) → 1b (workflow
execution) → 2e (terminal/git-changes mentions). Each phase is independently testable.
## Risks
- `FilenameIndex.processAllFileNames` cost on very large projects — mitigated by candidate cap + the
`GotoFileModel` escape hatch.
- `runBlockingCancellable` inside completion fill: safe on the background completion thread, but keep
the RPC fast and cancellable; never run on EDT.
- Provider must be `DumbAware` or completion is fully suppressed during indexing.
- `@terminal` scrollback depends on the optional terminal plugin and its (semi-internal) widget API —
keep it best-effort and warn if it relies on an experimental/internal IntelliJ API; hide the mention
when unavailable rather than failing.
- `gitChanges` via shelling `git diff` must use the project-spawn process util (Windows `windowsHide`)
and be scoped to `directory`; large diffs should be size-capped before embedding as a data URL.
@@ -394,8 +394,11 @@ class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi {
return WorkspaceFileDto(rel, vf.name, vf.isDirectory)
}
private fun gitAvailable(base: Path): Boolean = gitCache.getOrPut(base.toString()) {
Files.exists(base.resolve(".git")) || git(base, "rev-parse", "--is-inside-work-tree").trim() == "true"
private fun gitAvailable(base: Path): Boolean {
if (Files.exists(base.resolve(".git"))) return true
return gitCache.getOrPut(base.toString()) {
git(base, "rev-parse", "--is-inside-work-tree").trim() == "true"
}
}
private fun git(base: Path, vararg args: String): String {
@@ -6,45 +6,50 @@ import kotlin.test.assertEquals
import kotlin.test.assertNull
class WorkspacePathScopingTest {
private val base = Path.of("/home/user/project")
// Derive an absolute, OS-portable base from the real home dir so the test runs identically on
// Windows, macOS, and Linux (no hardcoded POSIX "/home/..." literals).
private val base: Path = Path.of(System.getProperty("user.home")).resolve("kilo-scope-test").normalize()
private fun at(vararg segments: String): Path = segments.fold(base) { acc, s -> acc.resolve(s) }
@Test
fun `in-base file returns forward-slash relative path`() {
assertEquals("src/A.kt", relativeWithinBase(base, Path.of("/home/user/project/src/A.kt")))
assertEquals("src/A.kt", relativeWithinBase(base, at("src", "A.kt")))
}
@Test
fun `nested file returns nested relative path`() {
assertEquals("a/b/c.kt", relativeWithinBase(base, Path.of("/home/user/project/a/b/c.kt")))
assertEquals("a/b/c.kt", relativeWithinBase(base, at("a", "b", "c.kt")))
}
@Test
fun `base itself is rejected as blank`() {
assertNull(relativeWithinBase(base, Path.of("/home/user/project")))
assertNull(relativeWithinBase(base, base))
}
@Test
fun `sibling directory outside base is rejected`() {
assertNull(relativeWithinBase(base, Path.of("/home/user/other/A.kt")))
assertNull(relativeWithinBase(base, base.resolveSibling("other").resolve("A.kt")))
}
@Test
fun `parent directory is rejected`() {
assertNull(relativeWithinBase(base, Path.of("/home/user")))
assertNull(relativeWithinBase(base, base.parent))
}
@Test
fun `traversal that escapes base is rejected after normalization`() {
assertNull(relativeWithinBase(base, Path.of("/home/user/project/../secret/A.kt")))
assertNull(relativeWithinBase(base, base.resolve("..").resolve("secret").resolve("A.kt")))
}
@Test
fun `traversal that stays inside base is kept after normalization`() {
assertEquals("src/A.kt", relativeWithinBase(base, Path.of("/home/user/project/x/../src/A.kt")))
assertEquals("src/A.kt", relativeWithinBase(base, base.resolve("x").resolve("..").resolve("src").resolve("A.kt")))
}
@Test
fun `prefix sibling is not treated as inside base`() {
assertNull(relativeWithinBase(base, Path.of("/home/user/project-2/A.kt")))
val sibling = base.resolveSibling(base.fileName.toString() + "-2")
assertNull(relativeWithinBase(base, sibling.resolve("A.kt")))
}
}