mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-10-30 19:32:39 +08:00
fix(jetbrains): clean up workspace path followups
This commit is contained in:
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
+5
-2
@@ -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 {
|
||||
|
||||
+14
-9
@@ -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")))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user