mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
fix(jetbrains): address prompt attachment review
This commit is contained in:
@@ -1,113 +0,0 @@
|
||||
# Chip-Based Prompt Attachments Plan
|
||||
|
||||
## Goal
|
||||
Implement JetBrains prompt attachments as Swing chips in the chat input, supporting file/image drag-and-drop, deletion, preview/open actions, and sending the correct CLI `file` parts. Keep this first pass out of editor inlays and out of any `@mention` autocomplete work.
|
||||
|
||||
## Scope
|
||||
- Add chips above the prompt editor inside the existing `PromptShell`.
|
||||
- Accept dropped files from OS and Project View via IntelliJ DnD APIs.
|
||||
- Send text plus attachment parts through the existing RPC prompt path.
|
||||
- Use `file://` URLs for dropped files to avoid copying file contents in the frontend.
|
||||
- Use `data:` URLs only for byte-only image drops if supported by the transferable; do not write standalone files at attach time.
|
||||
- Allow deleting chips before send.
|
||||
- Allow opening file-backed chips in the IntelliJ editor; preview image chips in Swing.
|
||||
- Defer `@mention`, draft persistence, and broad clipboard/paste support unless the implementation naturally shares the same extraction path.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### 1. Extend Prompt DTOs
|
||||
- Update `packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt` so `PromptPartDto` can represent both text and file parts.
|
||||
- Keep the shape simple with optional fields: `type`, `text`, `mime`, `url`, `filename`, and leave structured `source` for a future mention implementation.
|
||||
- Preserve existing text usage by making `PromptPartDto(type = "text", text = text)` still easy and compatible with current callers.
|
||||
- Update `ChatLogSummary.prompt(PromptDto)` to handle null `text`, count attachment parts, include MIME/type summaries, and avoid logging paths or base64 data.
|
||||
|
||||
### 2. Serialize File Parts To CLI JSON
|
||||
- Update `KiloCliDataParser.buildPromptJson` to emit per-part fields based on `part.type`:
|
||||
- Text: `{"type":"text","text":...}`.
|
||||
- File: `{"type":"file","mime":...,"url":...,"filename":...}` with optional fields omitted when null.
|
||||
- Keep the top-level prompt fields unchanged: `messageID`, `noReply`, `model`, `agent`, `variant`.
|
||||
- Add tests in `KiloCliDataParserTest` for mixed text/file prompts, file-only prompts, escaping, and optional filename omission.
|
||||
- Add a serialization round-trip test in `ChatDtoSerializationTest` for `PromptPartDto(type = "file", mime = "image/png", url = "file://...", filename = "...")`.
|
||||
|
||||
### 3. Thread Attachments Through Frontend Send Path
|
||||
- Add an optional attachment parameter to `SessionController.prompt`, e.g. `prompt(text: String, files: List<PromptPartDto> = emptyList())`, so existing tests and call sites remain valid.
|
||||
- Build prompt parts as text first when `text.isNotBlank()`, then file parts.
|
||||
- Update telemetry/logging to include safe counts like `attachmentCount` and `mediaAttachmentCount`, not file paths or `data:` payloads.
|
||||
- Update `SessionUi.sendPrompt` and `PromptPanel` callback from text-only to text plus attachment parts.
|
||||
- Keep existing clear-on-send behavior: capture text and attachments, clear the prompt UI, dispatch the prompt, and preserve scroll-follow behavior.
|
||||
|
||||
### 4. Add Prompt Attachment State And Chips
|
||||
- Add prompt-local frontend code under `frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/` for attachment state and chip rendering.
|
||||
- Suggested small types:
|
||||
- `PromptAttachment`: id, display name, MIME, URL, optional `Path`, optional decoded image bytes/thumbnail metadata.
|
||||
- `PromptAttachmentChip`: retained Swing component showing icon/thumbnail, filename, secondary MIME/path hint if useful, close button, and click/open behavior.
|
||||
- `PromptAttachmentStrip`: retained wrapping chip row that adds/removes chip components without rebuilding the prompt shell.
|
||||
- Place the strip above the editor inside `PromptShell`; hide it when empty.
|
||||
- Update `PromptPanel.isSendEnabled` so attachment-only prompts can be sent when ready and not busy.
|
||||
- Update `PromptPanel.clear()` to clear both text and attachments.
|
||||
- Use platform components/styles: `JBLabel`, `HoverIcon` or existing icon-button helpers, `JBUI` borders/insets, `UIUtil`/`JBUI.CurrentTheme` colors, and `SessionUiStyle`/`UiStyle` where existing constants fit.
|
||||
|
||||
### 5. Drag-And-Drop Extraction
|
||||
- Install DnD on the prompt shell/editor area with public IntelliJ APIs:
|
||||
- `DnDSupport.createBuilder(shell)`.
|
||||
- `enableAsNativeTarget()`.
|
||||
- `setTargetChecker { event -> ... }` to mark file/image drops as possible.
|
||||
- `setDropHandlerWithResult { event -> ... }` to add attachments and report success.
|
||||
- Use `FileCopyPasteUtil.getFileList(event)` or `FileCopyPasteUtil.getFileListFromAttachedObject(event.attachedObject)` for file drops.
|
||||
- Convert dropped filesystem entries to `file://` attachment parts.
|
||||
- Derive MIME cheaply and safely:
|
||||
- Directory: `application/x-directory` if accepted in the first pass, otherwise skip with a user-visible message.
|
||||
- Common images: `image/png`, `image/jpeg`, `image/gif`, `image/webp`, `image/bmp`, `image/svg+xml` by extension.
|
||||
- PDF: `application/pdf`.
|
||||
- Non-binary IntelliJ file types: `text/plain`.
|
||||
- Fallback: `application/octet-stream`.
|
||||
- Resolve `VirtualFile` via `LocalFileSystem`/`VfsUtil` only when needed for icons/opening, and avoid blocking EDT for expensive refreshes.
|
||||
- If the transferable exposes `DataFlavor.imageFlavor` without a file path, encode it as a `data:image/png;base64,...` chip if practical; otherwise defer byte-only image drops to a follow-up.
|
||||
|
||||
### 6. Preview And Open Behavior
|
||||
- For file-backed chips, click/open should resolve the path to a `VirtualFile` and call `FileEditorManager.getInstance(project).openFile(file, true)` on EDT.
|
||||
- For image file chips, show a thumbnail in the chip and use the same editor open path; IntelliJ's image editor handles the full preview.
|
||||
- For `data:` image chips, preview in a lightweight Swing popup/dialog using `ImageLoader.loadFromBytes` and scaled `JBImageIcon`.
|
||||
- If an `Open in Editor` action is required for `data:` images, lazily write a temp file only when the user opens it, refresh VFS, and open it with `FileEditorManager`; do not persist temp files as part of attaching/sending.
|
||||
- If a file is missing by the time the user opens/sends, keep the chip removable and surface a non-modal notification or inline error string.
|
||||
|
||||
### 7. Model Capability Handling
|
||||
- Copy `ModelDto.attachment` into `ModelItem` during workspace provider mapping.
|
||||
- Gate media attachments on the selected model's attachment capability where feasible.
|
||||
- Allow text file references even when media attachment capability is false.
|
||||
- If a user drops unsupported media for the current model, reject it with a localized message instead of sending a prompt that the model cannot handle.
|
||||
|
||||
### 8. User-Visible Strings
|
||||
- Add base English keys to `frontend/src/main/resources/messages/KiloBundle.properties` for:
|
||||
- Drop hint/target text.
|
||||
- Remove attachment accessible name.
|
||||
- Open/preview attachment accessible name.
|
||||
- Unsupported attachment/model messages.
|
||||
- Missing file message.
|
||||
- Rely on ResourceBundle fallback for localized bundles unless the repo convention requires adding placeholders to every locale.
|
||||
|
||||
### 9. Tests
|
||||
- Backend/shared:
|
||||
- `KiloCliDataParserTest`: mixed text/file JSON, file-only JSON, escaping, optional fields.
|
||||
- `ChatDtoSerializationTest`: file prompt part round-trip.
|
||||
- `ChatLogSummaryTest`: file parts are summarized without leaking paths/base64.
|
||||
- Frontend controller:
|
||||
- Add/update prompt lifecycle tests to assert `SessionController.prompt("see", files)` sends text plus file parts and preserves model/agent/variant fields.
|
||||
- Ensure existing `prompt("text")` callers remain unchanged.
|
||||
- Prompt UI:
|
||||
- `PromptPanelTest`: chip row starts hidden, adding an attachment shows it, delete hides/removes chip, `clear()` removes chips, attachment-only prompt enables send, busy state disables send.
|
||||
- If practical, test DnD extraction logic separately as a small pure helper using real temporary files.
|
||||
|
||||
### 10. Verification
|
||||
- Run targeted tests first from `packages/kilo-jetbrains/`:
|
||||
- `./gradlew :backend:test --tests ai.kilocode.backend.cli.KiloCliDataParserTest --tests ai.kilocode.backend.cli.ChatDtoSerializationTest --tests ai.kilocode.backend.cli.ChatLogSummaryTest`
|
||||
- `./gradlew :frontend:test --tests ai.kilocode.client.session.ui.PromptPanelTest --tests ai.kilocode.client.session.controller.PromptLifecycleTest`
|
||||
- Run `./gradlew typecheck` or `bun run typecheck` from `packages/kilo-jetbrains/` before declaring implementation complete.
|
||||
- Add a patch changeset because this is user-facing JetBrains plugin functionality.
|
||||
|
||||
## Non-Goals For This Pass
|
||||
- Editor inlay attachments.
|
||||
- A VS Code-style `@mention` system.
|
||||
- Persisting prompt draft attachments across IDE restart/session switch.
|
||||
- Reworking CLI attachment resolution or shared opencode files.
|
||||
- Broad clipboard handling beyond any low-risk byte-only image support that falls out of the DnD extraction helper.
|
||||
@@ -1,58 +0,0 @@
|
||||
# Plan: Move Prompt Drop Work Off EDT
|
||||
|
||||
## Goal
|
||||
Prevent the JetBrains prompt attachment drop path from freezing the EDT by moving blocking file work off the UI thread while preserving Swing mutations on the EDT.
|
||||
|
||||
## Findings
|
||||
- `PromptPanel.installDnD()` handles drops synchronously in `setDropHandlerWithResult`.
|
||||
- The current drop callback does:
|
||||
- `dropFiles(event)` using `FileCopyPasteUtil.getFileListFromAttachedObject(event.attachedObject)`.
|
||||
- `PromptAttachmentExtractor.files(files)`.
|
||||
- `items.forEach(::addAttachment)`.
|
||||
- `PromptAttachmentExtractor.files()` performs filesystem calls that can block:
|
||||
- `file.exists()`.
|
||||
- `file.isDirectory` inside mime detection.
|
||||
- path normalization / URI construction for every file.
|
||||
- UI work that must stay on EDT:
|
||||
- `addAttachment()` because it mutates `attachments`, `strip`, notifications, and calls `onChange()`.
|
||||
- `setDropPossible()` / target checker UI feedback.
|
||||
- Swing repaint/revalidate work.
|
||||
- IntelliJ platform examples usually extract the dropped `File` list immediately in the drop callback, then schedule heavier handling. Because native `Transferable` lifetime can be subtle, the safer first fix is to keep file-list extraction immediate but move metadata/existence/mime work off EDT.
|
||||
|
||||
## Implementation Plan
|
||||
1. Update `PromptPanel` drop handling.
|
||||
- Keep `setTargetChecker` as-is except avoid any filesystem work.
|
||||
- In `setDropHandlerWithResult`, capture `area` and extract the initial `List<java.io.File>` immediately.
|
||||
- If the list is empty, return `false` as before.
|
||||
- If non-empty, log `kind=prompt-dnd drop area=... files=... queued=true` and schedule background processing.
|
||||
- Return `true` immediately so DnD completes without waiting for metadata work.
|
||||
|
||||
2. Add a background processing helper in `PromptPanel`.
|
||||
- Use `ApplicationManager.getApplication().executeOnPooledThread { ... }` for minimal constructor churn.
|
||||
- Run `PromptAttachmentExtractor.files(files)` on the pooled thread.
|
||||
- Measure elapsed time for file-list extraction and metadata extraction with `System.nanoTime()` or `measureTime`.
|
||||
- Catch exceptions and log them with `LOG.warn` or `LOG.error`; do not silently swallow failures.
|
||||
|
||||
3. Return to EDT for UI mutation.
|
||||
- Use `ApplicationManager.getApplication().invokeLater { ... }` after background extraction.
|
||||
- Before mutating UI, check `project.isDisposed` and `isDisplayable` or an equivalent component lifecycle guard.
|
||||
- Add extracted items with `items.forEach(::addAttachment)` on EDT.
|
||||
- Log `kind=prompt-dnd attach area=... files=... attachments=... extractMs=...`.
|
||||
|
||||
4. Keep unsupported-model and duplicate behavior on EDT.
|
||||
- Do not move `attachment` checks or `attachments.any { ... }` to background because those are UI state.
|
||||
- This preserves the existing model gating and duplicate behavior.
|
||||
|
||||
5. Add/adjust tests.
|
||||
- Add a focused unit test for the async helper if practical without relying on real DnD events.
|
||||
- Keep existing `PromptPanelTest` coverage for attachments and duplicate/removal behavior.
|
||||
- If direct async testing would require fragile sleeps, prefer extracting a small background scheduling helper with a test-visible callback only if it remains simple and does not add production-only test plumbing.
|
||||
|
||||
6. Verification.
|
||||
- Run `./gradlew :frontend:test --tests ai.kilocode.client.session.ui.PromptPanelTest` from `packages/kilo-jetbrains/`.
|
||||
- Run `./gradlew typecheck` from `packages/kilo-jetbrains/`.
|
||||
|
||||
## Follow-Up If Freeze Persists
|
||||
- Use the new timing logs to identify whether the remaining synchronous `dropFiles(event)` call is the freeze source.
|
||||
- If `dropFiles` is slow, escalate to capturing `event.attachedObject` in the drop callback and calling `FileCopyPasteUtil.getFileListFromAttachedObject(attached)` on the pooled thread.
|
||||
- That escalation is riskier because native transfer data may not always remain valid after the drop callback returns, so it should be driven by timing evidence.
|
||||
@@ -1,60 +0,0 @@
|
||||
# JetBrains Attachment Rendering Plan
|
||||
|
||||
## Goal
|
||||
Make JetBrains prompt attachments visually match the VS Code-style attachment cards/chips, add clearer tooltips, show the delete affordance only on hover in the input prompt, and render sent prompt attachments in the session transcript as attachment UI instead of raw/JSON-like payload text.
|
||||
|
||||
## Current Findings
|
||||
- Prompt input already has `PromptAttachment`, `PromptAttachmentExtractor`, and `PromptAttachmentStrip` in `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/`.
|
||||
- `PromptAttachmentChip` currently renders a bordered Swing panel with icon/name and an always-visible close button. Its tooltip is only the MIME type.
|
||||
- Prompt submission already sends file parts as `PromptPartDto(type = "file", mime, url, filename)`.
|
||||
- Backend prompt JSON serialization already preserves file parts, but shared `PartDto` only has generic fields and no `mime`, `url`, or `filename` fields.
|
||||
- The frontend session model maps unknown part types to `Generic`, so returned/history `file` parts are not first-class and cannot render like prompt attachments.
|
||||
- User prompt transcript rendering routes text through `PromptView`; non-text user content falls back through the normal `ViewFactory` path.
|
||||
|
||||
## Implementation Steps
|
||||
1. Preserve file metadata in JetBrains DTOs and parser.
|
||||
- Add optional `mime`, `url`, and `filename` fields to `PartDto` in `packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt`.
|
||||
- Update `KiloCliDataParser.parsePart()` to populate those fields from JSON file parts. Use `mime`, `url`, and `filename` keys from the backend payload.
|
||||
- Add/adjust backend serialization/parser tests so a `type=file` `PartDto` round-trips and parses with metadata intact.
|
||||
|
||||
2. Add first-class model content for file attachments.
|
||||
- Add a `FileAttachment` or similarly named `Content` subtype in `frontend/src/main/kotlin/ai/kilocode/client/session/model/Message.kt` with `mime`, `url`, and `filename`.
|
||||
- Update `SessionModel.fromDto()` to map `dto.type == "file"` into this subtype.
|
||||
- Update `SessionModel.updateExisting()` to refresh file metadata without replacing the content when the same part id updates.
|
||||
- Update `timelineTitle()`, `weight()`, and `renderMessage()` to handle the new subtype. The test string can be concise, for example `file#p1 image/png a.png`.
|
||||
- Keep unknown non-file types falling back to `Generic`.
|
||||
|
||||
3. Extract/reuse attachment UI for prompt input and transcript.
|
||||
- Refactor `PromptAttachmentStrip.kt` so the reusable chip/card component is not private to the prompt strip, or create a new `AttachmentChip`/`AttachmentView` in the same UI area.
|
||||
- Keep it Swing-only and theme-aware: use `JBLabel`, `JButton`/`HoverIcon`, `JBUI` borders/insets, platform colors/icons, and no hardcoded runtime colors.
|
||||
- Render attachments as compact rectangular cards/chips with an icon and filename, keeping the visual language close to VS Code’s attachment rectangles while fitting IntelliJ Swing.
|
||||
- Add clearer tooltips, e.g. include filename, MIME type, and URL/path when available. Add localized strings in `KiloBundle.properties`, such as `prompt.attachment.tooltip` and a transcript-safe open tooltip.
|
||||
- Make the remove/delete icon visible only when hovering the prompt attachment card. Do not show a delete icon for sent prompt transcript attachments.
|
||||
- Preserve click-to-open behavior for local `file://` attachments when a project is available. For non-local/data URLs, either no-op with tooltip context or route through `openUrl` if appropriate.
|
||||
|
||||
4. Render file parts in the session transcript.
|
||||
- Add a new `PartView` for the file attachment content, likely `AttachmentView.kt` under `frontend/src/main/kotlin/ai/kilocode/client/session/views/`.
|
||||
- In `ViewFactory.createUser()`, route the new file content subtype to this attachment view so user prompt attachments render inside the same rounded user prompt bubble as the input attachments.
|
||||
- Also add a normal `ViewFactory.create()` branch for completeness, in case file parts appear outside user messages.
|
||||
- Ensure transcript attachment views are non-deletable but still have the improved tooltip and click/open behavior.
|
||||
|
||||
5. Tests.
|
||||
- Add/extend `SessionModelTest` for `type=file` mapping, metadata updates, and model dump output.
|
||||
- Add/extend `SessionUiUpdateTest` or `SessionMessageListPanelTest` to assert a user message with a file part renders the new attachment `PartView`, not `GenericView` or markdown JSON.
|
||||
- Add `ViewFactory`/view tests for the attachment view’s `dumpLabel`, tooltip contents, and non-deletable transcript mode.
|
||||
- Extend `PromptPanelTest` or add focused strip tests to assert the remove button is initially hidden and becomes visible on hover, while still removing the attachment when clicked.
|
||||
- Extend backend parser/serialization tests for file part metadata in `PartDto`.
|
||||
|
||||
6. Changeset.
|
||||
- Add a patch changeset for the JetBrains plugin user-facing fix, with wording like: `Render file attachments as attachment cards in JetBrains prompts and session history.`
|
||||
|
||||
## Verification
|
||||
Run from `packages/kilo-jetbrains/`:
|
||||
- Targeted Gradle tests for touched frontend/backend test classes, if supported by the local Gradle setup, e.g. `./gradlew test --tests "*PromptPanelTest" --tests "*SessionModelTest" --tests "*SessionMessageListPanelTest" --tests "*KiloCliDataParserTest" --tests "*ChatDtoSerializationTest"`.
|
||||
- `bun run typecheck` or `./gradlew typecheck` for the full JetBrains Kotlin compile.
|
||||
|
||||
## Notes And Constraints
|
||||
- Stay within `packages/kilo-jetbrains/`; no `kilocode_change` markers are needed because this package is Kilo-specific.
|
||||
- Keep Swing updates EDT-safe and use retained components rather than rebuilding whole trees on hover/update.
|
||||
- Do not introduce Compose, JCEF, or Kotlin UI DSL.
|
||||
- Prefer minimal reusable extraction from the existing prompt attachment strip instead of creating a separate design system for attachments.
|
||||
@@ -1,68 +0,0 @@
|
||||
# JetBrains Attachment Preview Card Plan
|
||||
|
||||
## Goal
|
||||
Render JetBrains attachments as rectangular preview cards matching the provided screenshot:
|
||||
- Prompt input attachments show a card with a preview area and a visible close button to remove the attachment.
|
||||
- Chat transcript attachments use the same card visual treatment and preview behavior, but never show a close/remove button.
|
||||
- Existing attachment metadata parsing/model work remains intact; this plan refines the frontend rendering.
|
||||
|
||||
## Current Findings
|
||||
- Prompt attachments are rendered by `PromptAttachmentStrip.kt` through the reusable `AttachmentChip` class.
|
||||
- Transcript file parts are rendered by `AttachmentView.kt`, which currently reuses `AttachmentChip` without a remove callback.
|
||||
- `PromptAttachment` provides local `path` for prompt-added files; transcript `FileAttachment` provides `mime`, `url`, and `filename`.
|
||||
- `ViewFactory` already routes `FileAttachment` to `AttachmentView` for both user and assistant/normal content paths.
|
||||
- There is no existing image thumbnail card implementation in the JetBrains frontend.
|
||||
- `packages/kilo-jetbrains/AGENTS.md` requires Swing-only UI, EDT-only UI mutation, retained component updates, and theme-derived colors/borders via `UiStyle`/`SessionUiStyle`.
|
||||
|
||||
## Implementation Steps
|
||||
1. Replace the compact attachment chip with a reusable preview card.
|
||||
- Create or move the shared UI out of prompt-specific naming, for example `frontend/src/main/kotlin/ai/kilocode/client/session/ui/attachment/AttachmentCard.kt`.
|
||||
- Rename the item DTO to something like `AttachmentCardItem` with `name`, `mime`, `url`, and optional local `path`.
|
||||
- Keep prompt-specific open/remove wiring in `PromptAttachmentStrip.kt`; keep transcript-specific opening in `AttachmentView.kt`.
|
||||
- Preserve tooltips and accessibility labels using existing `prompt.attachment.*` bundle keys unless a transcript-specific string is needed.
|
||||
|
||||
2. Design the card layout.
|
||||
- Use a retained Swing component, not a render/remove/recreate loop.
|
||||
- Use a fixed DPI-scaled card size through `SessionUiStyle.View.Attachment` tokens, e.g. card width/height, preview height, close button size, and corner arc.
|
||||
- Paint a rounded rectangular surface using theme-derived colors from `SessionUiStyle.View.surface()`, `SessionUiStyle.View.line()`, or `UiStyle.Colors.*`.
|
||||
- The card should contain a large preview area plus compact filename/mime text, with long names safely clipped by Swing layout rather than overflowing.
|
||||
- The prompt remove button should be a visible circular/hover-style overlay pinned to the top-right corner and should not shift layout when hovered.
|
||||
- When `remove == null`, omit the close button entirely so transcript attachments are not removable.
|
||||
|
||||
3. Add preview behavior.
|
||||
- For local image attachments (`mime.startsWith("image/")` and a prompt `path` or transcript `file://` URL), load and scale a thumbnail off the EDT using `ApplicationManager.getApplication().executeOnPooledThread`.
|
||||
- Update Swing labels/icons only back on the EDT with `invokeLater`, and guard against disposed/non-displayable/stale cards before applying the thumbnail.
|
||||
- Use `ImageIO`/`ImageIcon` only for supported local formats; gracefully fall back for SVG, unreadable paths, remote URLs, data URLs, directories, PDFs, and unknown files.
|
||||
- Fallback preview should use the existing MIME-based platform icon logic (`AllIcons.FileTypes.Image`, folder, text/file icon) centered in the preview area.
|
||||
- Do not fetch remote URLs for thumbnails.
|
||||
|
||||
4. Update prompt input wiring.
|
||||
- `PromptAttachmentStrip.add()` should create the new card with `path = item.path`, `remove = { removed(item) }`, and existing local-file open behavior.
|
||||
- Replace the current hover-only remove behavior with a visible close button matching the screenshot.
|
||||
- Keep duplicate prevention, clear/remove behavior, drag/drop handling, prompt sending, and tooltip content unchanged.
|
||||
|
||||
5. Update transcript rendering.
|
||||
- `AttachmentView` should create the same card with `remove = null`.
|
||||
- Continue opening `file://` URLs through `openFile(path)` and non-file URLs through `openUrl(url)`.
|
||||
- Continue retained update behavior: if metadata changes, update or replace only the child card as needed; do not rebuild parent message views.
|
||||
- Ensure `AttachmentView.dumpLabel()` remains stable for existing tests.
|
||||
|
||||
6. Update tests.
|
||||
- Update `PromptPanelTest` attachment test to assert the remove button is visible and clickable without requiring hover.
|
||||
- Add/adjust assertions that prompt attachment cards expose the richer tooltip and contain a card-sized preview component.
|
||||
- Add/adjust transcript UI assertions in `SessionUiUpdateTest` to confirm file parts render as `AttachmentView` and contain no remove button.
|
||||
- Keep existing parser/serialization/model tests unless frontend changes require expected string updates.
|
||||
- Avoid brittle async image-thumbnail assertions unless the implementation provides a deterministic, real component state that can be tested without sleeps.
|
||||
|
||||
7. Verification.
|
||||
- Run targeted JetBrains tests from `packages/kilo-jetbrains/`:
|
||||
- `./gradlew test --tests '*PromptPanelTest' --tests '*SessionUiUpdateTest' --tests '*SessionModelTest' --tests '*KiloCliDataParserTest' --tests '*ChatDtoSerializationTest'`
|
||||
- Run full JetBrains typecheck from `packages/kilo-jetbrains/`:
|
||||
- `./gradlew typecheck` or `bun run typecheck`
|
||||
|
||||
## Constraints
|
||||
- Stay within `packages/kilo-jetbrains/`; no `kilocode_change` markers are needed.
|
||||
- Do not modify DTO/parser/model behavior unless a compile or test failure exposes a gap.
|
||||
- Do not introduce Compose, JCEF, Kotlin UI DSL, remote thumbnail fetching, or new third-party dependencies.
|
||||
- Keep all Swing creation/mutation/access on the EDT, except image decoding/scaling work which must run off the EDT and return to the EDT for UI mutation.
|
||||
- Use `JBUI`, `UiStyle`, and `SessionUiStyle` for spacing, dimensions, borders, colors, and fonts.
|
||||
@@ -1,102 +0,0 @@
|
||||
# Plan: Session File Drop Overlay
|
||||
|
||||
## Goal
|
||||
Show a full-session drag overlay when the JetBrains session UI can accept dragged files, darkening the session surface and displaying a centered message such as `Drop files here to add them to the prompt`. Dropping files anywhere on the session should add them to the current prompt using the existing attachment flow.
|
||||
|
||||
## Findings
|
||||
- Existing file drag/drop is already implemented in `PromptPanel` with IntelliJ `DnDSupport`, `FileCopyPasteUtil`, and `PromptAttachmentExtractor`.
|
||||
- Current DnD targets are prompt-local only: editor content, editor scroll pane, and prompt shell.
|
||||
- Accepted drag detection currently means `FileCopyPasteUtil.isFileListFlavorAvailable(event)` is true. Actual attachment filtering happens after drop through `PromptAttachmentExtractor.files(files)` and `PromptPanel.addAttachment`.
|
||||
- `SessionUi` owns the full session surface through `SessionRootPanel`, which extends `LayeredOverlayPanel` and already supports session-wide overlays via `root.addOverlay(...)`.
|
||||
- `LayeredOverlayPanel.Overlay.contains(...)` only captures pointer events for visible overlay children, so a hidden full-size drop overlay will not block normal UI interaction.
|
||||
|
||||
## Scope
|
||||
Expected files to change:
|
||||
|
||||
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt`
|
||||
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionDropOverlay.kt`
|
||||
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt`
|
||||
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt`
|
||||
- `packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties`
|
||||
- `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt`
|
||||
- `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionRootPanelTest.kt` or a new focused overlay test
|
||||
- `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt`
|
||||
|
||||
No backend, RPC, DTO, or send-path changes are needed.
|
||||
|
||||
## Implementation Steps
|
||||
1. Add a retained Swing `SessionDropOverlay` component:
|
||||
- Place it under `ai.kilocode.client.session.ui`.
|
||||
- Extend a standard Swing/JB panel, keep it hidden by default, and make it non-opaque.
|
||||
- Override painting to fill the full bounds with a theme-owned translucent scrim color.
|
||||
- Render a centered `JBLabel` with a bundled string: `Drop files here to add them to the prompt`.
|
||||
- Add an EDT-only method such as `setActive(value: Boolean)` that toggles visibility, revalidates, and repaints only when the state changes.
|
||||
- Set the accessible name to the same bundled text.
|
||||
|
||||
2. Add session-specific style tokens:
|
||||
- Add a `SessionUiStyle.View.DropOverlay` object for scrim color, content padding, corner arc, and any centered-card sizing.
|
||||
- Keep hardcoded alpha/color construction centralized in the style object, not inline in the component.
|
||||
- Use existing `UiStyle`/platform colors for label foreground and panel/card surfaces.
|
||||
|
||||
3. Add bundle text:
|
||||
- Add a key like `session.drop.files=Drop files here to add them to the prompt` in `KiloBundle.properties`.
|
||||
- Keep all visible text out of Kotlin source.
|
||||
|
||||
4. Refactor prompt file-drop handling without changing behavior:
|
||||
- Turn `PromptPanel.installDnD(...)` into an internal reusable method, for example `installFileDrop(target: JComponent, area: String)`.
|
||||
- Keep `dropFiles(...)`, `processDrop(...)`, duplicate checks, model media blocking, notifications, and `onChange()` behavior centralized in `PromptPanel`.
|
||||
- Add a callback property such as `onFileDrag: (Boolean) -> Unit = {}`.
|
||||
- In `setTargetChecker`, call the callback with `true` only when file-list flavor is available and `event.setDropPossible(true)` is set.
|
||||
- Call the callback with `false` for non-file drags, clean-up-on-leave, empty/failed drops, and after a handled drop.
|
||||
- Keep the existing prompt/editor/shell DnD targets so drag feedback still works over editor internals.
|
||||
|
||||
5. Install session-wide DnD and overlay in `SessionUi`:
|
||||
- Create `private lateinit var drop: SessionDropOverlay`.
|
||||
- Add it to `root` with `root.addOverlay(drop) { pane, _ -> Rectangle(0, 0, pane.width, pane.height) }`.
|
||||
- Put it at top z-order inside `root.overlay` so it darkens account/jump overlays too while active.
|
||||
- Wire `prompt.onFileDrag = { drop.setActive(it) }`.
|
||||
- Install the same prompt file-drop handler on `root` for initial session-wide detection.
|
||||
- Also install it on the drop overlay itself so once the overlay becomes visible and topmost, drag-over/drop events continue to be accepted and delegated to the prompt.
|
||||
|
||||
6. Preserve current attachment semantics:
|
||||
- Do not add files directly from `SessionUi`; always delegate to `PromptPanel` so all existing validation and prompt state updates remain identical.
|
||||
- Continue accepting any native file-list drag at the DnD level. If the selected model rejects image/PDF attachments, keep the existing post-drop warning path.
|
||||
- Avoid file-system extraction in `setTargetChecker`; do extraction only in the drop handler on the pooled thread as it works today.
|
||||
|
||||
## Test Plan
|
||||
1. `SessionRootPanelTest` or a new `SessionDropOverlayTest`:
|
||||
- Assert the drop overlay starts hidden.
|
||||
- Assert `setActive(true)` makes it visible and `setActive(false)` hides it.
|
||||
- Assert a full-size overlay child receives full root bounds after layout.
|
||||
- Assert the overlay component is in the overlay layer, not the blocker layer.
|
||||
|
||||
2. `SessionUiLayoutTest`:
|
||||
- Assert `SessionDropOverlay` is attached under `SessionRootPanel.overlay`.
|
||||
- Assert it covers the whole root after layout.
|
||||
- Assert its z-order is above existing account/scroll overlays.
|
||||
- Assert the bottom stack still contains only `ConnectionPanel` and `PromptPanel`.
|
||||
|
||||
3. `PromptPanelTest`:
|
||||
- Add focused tests around the new file-drag callback method using a test hook only if needed for DnD lifecycle; prefer exercising the reusable installer/callback indirectly where possible.
|
||||
- Keep existing attachment tests unchanged for actual add/remove/send behavior.
|
||||
- Add extractor tests if needed for accepted file types, but do not duplicate MIME logic in tests.
|
||||
|
||||
4. Manual sandbox check after automated tests:
|
||||
- Drag a local file over transcript/header area: overlay appears.
|
||||
- Drop it over transcript/header area: attachment appears in the prompt strip.
|
||||
- Drag a local file over prompt/editor area: overlay appears.
|
||||
- Drag non-file content: overlay stays hidden and drop is not accepted.
|
||||
- Drag away or press escape: overlay hides.
|
||||
|
||||
## Verification
|
||||
Run the smallest relevant JetBrains checks after implementation:
|
||||
|
||||
- From `packages/kilo-jetbrains/`: `./gradlew :frontend:test --tests '*PromptPanelTest' --tests '*SessionRootPanelTest' --tests '*SessionUiLayoutTest'`
|
||||
- From `packages/kilo-jetbrains/`: `./gradlew typecheck`
|
||||
|
||||
## Notes
|
||||
- Swing/model access must stay EDT-only.
|
||||
- Use `DnDSupport.setCleanUpOnLeaveCallback` to hide the overlay on drag leave.
|
||||
- Use `DnDSupport.setDropHandlerWithResult` to hide the overlay before/after handling the drop.
|
||||
- The overlay should be visual feedback only; it should not own attachment state.
|
||||
- If root-level native DnD does not fire over a particular child in sandbox testing, install the same reusable prompt file-drop handler on that child rather than adding a separate attachment path.
|
||||
@@ -1,96 +0,0 @@
|
||||
# Plan: JetBrains Prompt Paste Attachments
|
||||
|
||||
## Goal
|
||||
Add paste-to-attach support for the JetBrains chat prompt so clipboard file/image content is attached to the prompt the same way native file drag/drop currently adds attachments, while normal text paste continues to behave like the default IntelliJ editor paste.
|
||||
|
||||
## Findings
|
||||
- Current attachment state and validation live in `PromptPanel`:
|
||||
- `installFileDrop(...)` accepts native file DnD with `FileCopyPasteUtil`.
|
||||
- `processDrop(...)` extracts attachments on a pooled thread and returns to EDT before `addAttachment(...)`.
|
||||
- `addAttachment(...)` handles duplicate blocking, model media capability blocking, attachment strip updates, and `onChange()`.
|
||||
- `PromptAttachmentExtractor.files(...)` already converts `java.io.File` values into `PromptAttachment` objects and should remain the file attachment path for both drag/drop and paste.
|
||||
- The local IntelliJ checkout is available at `/Users/kirillk/products/intellij-community`.
|
||||
- IntelliJ reference APIs:
|
||||
- `com.intellij.ide.dnd.FileCopyPasteUtil.getFileList(Transferable)` handles clipboard file lists including platform-specific URI flavors.
|
||||
- `com.intellij.ide.PasteProvider` is the public paste interception API.
|
||||
- `com.intellij.openapi.editor.actions.PasteAction.TRANSFERABLE_PROVIDER` exposes the actual transferable being pasted from the editor paste flow.
|
||||
- `com.intellij.customPasteProvider` is the editor custom paste extension point used before default text paste.
|
||||
- IntelliJ reference examples:
|
||||
- `plugins/agent-workbench/prompt/ui/src/context/AgentPromptImagePasteProvider.kt` registers a `customPasteProvider`, gates it to prompt editors with an editor user-data handler key, and handles `DataFlavor.imageFlavor`.
|
||||
- `plugins/markdown/images/src/main/kotlin/org/intellij/plugins/markdown/images/editor/paste/FileLinkPasteProvider.kt` uses `FileCopyPasteUtil.getFiles(...)`/`getFileList(...)` from the paste `Transferable`.
|
||||
- Kilo server prompt resolution already accepts non-text `data:` file URLs, so a raw clipboard image can be attached as a `data:image/png;base64,...` `PromptPartDto` without writing a temp file.
|
||||
|
||||
## Scope
|
||||
Expected files to change:
|
||||
|
||||
- `packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml`
|
||||
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt`
|
||||
- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/PromptAttachment.kt`
|
||||
- New `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptAttachmentPasteProvider.kt`
|
||||
- `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt`
|
||||
- New `.changeset/<slug>.md`
|
||||
|
||||
No backend, RPC, SDK, or server protocol changes are expected.
|
||||
|
||||
## Implementation Steps
|
||||
1. Add a prompt-scoped paste handler contract:
|
||||
- In a new `PromptAttachmentPasteProvider.kt`, define an internal `fun interface` for handling a `Transferable`.
|
||||
- Define an internal `Key<...>` for storing that handler on the editor, mirroring IntelliJ Agent Workbench’s prompt image paste pattern.
|
||||
|
||||
2. Register an editor custom paste provider:
|
||||
- Add `<customPasteProvider implementation="ai.kilocode.client.session.ui.prompt.PromptAttachmentPasteProvider"/>` under `kilo.jetbrains.frontend.xml` extensions.
|
||||
- Implement `PasteProvider` so it activates only when `CommonDataKeys.EDITOR` has the prompt handler key.
|
||||
- In `isPastePossible`/`isPasteEnabled`, inspect `PasteAction.TRANSFERABLE_PROVIDER.produce()` and return true only for native file-list flavors or `DataFlavor.imageFlavor`.
|
||||
- In `performPaste`, pass the transferable to the prompt handler and return without touching document text.
|
||||
- Because the provider returns false for normal text, default IntelliJ text paste remains unchanged.
|
||||
|
||||
3. Wire the handler into `PromptPanel`:
|
||||
- In the existing editor `addSettingsProvider`, store the prompt paste handler on `EditorEx` via the new key.
|
||||
- Add a `processPaste(transferable)` method in `PromptPanel` that mirrors the existing DnD async flow.
|
||||
- Refactor the common attachment processing behind drop and paste into one private method if it keeps the code smaller and avoids duplicated logging/error handling.
|
||||
- Keep all UI mutation on EDT and all file/image extraction off EDT.
|
||||
|
||||
4. Support clipboard file lists:
|
||||
- Use `FileCopyPasteUtil.getFileList(transferable).orEmpty()`.
|
||||
- Convert files through `PromptAttachmentExtractor.files(files)`.
|
||||
- Add them through existing `addAttachment(...)` so duplicates and unsupported-model warnings remain identical to drag/drop.
|
||||
|
||||
5. Support raw clipboard images:
|
||||
- Add an extractor helper that accepts a clipboard image object (`BufferedImage`, `MultiResolutionImage`, or `Image`) and creates a `PromptAttachment` with:
|
||||
- `mime = "image/png"`
|
||||
- `name` like `pasted-image.png` or a timestamped variant
|
||||
- `url = "data:image/png;base64,..."`
|
||||
- a unique `id` so repeated image pastes are allowed
|
||||
- Convert non-buffered images to `BufferedImage` using the same approach as the IntelliJ Agent Workbench reference.
|
||||
- Encode with `ImageIO.write(..., "png", ...)` and `Base64`.
|
||||
- Let existing `addAttachment(...)` block the image with the current model capability warning when image/PDF attachments are disabled.
|
||||
|
||||
6. Add tests in `PromptPanelTest`:
|
||||
- Test file-list paste:
|
||||
- Create a temp file.
|
||||
- Put a custom `Transferable` supporting `DataFlavor.javaFileListFlavor` into the editor paste provider path.
|
||||
- Invoke `PromptAttachmentPasteProvider.performPaste(...)` or the registered handler path with a real `DataContext`.
|
||||
- Wait for the pooled extraction and EDT callback, then assert the attachment count increments.
|
||||
- Test raw image paste:
|
||||
- Use a small `BufferedImage` transferable supporting `DataFlavor.imageFlavor`.
|
||||
- Assert an image attachment is added.
|
||||
- Test normal text paste is not intercepted:
|
||||
- Use a string-only transferable and assert the custom provider is not enabled, so default editor paste can proceed.
|
||||
- Test unsupported media model behavior if practical:
|
||||
- Set `setAttachmentEnabled(false)`, paste an image, flush async work, and assert the attachment count remains unchanged.
|
||||
|
||||
7. Add a changeset:
|
||||
- Create a patch changeset for `@kilocode/kilo-jetbrains`.
|
||||
- Suggested user-facing text: `Support pasting files and images into JetBrains chat prompts as attachments.`
|
||||
|
||||
## Verification
|
||||
Run the smallest relevant checks after implementation:
|
||||
|
||||
- From `packages/kilo-jetbrains/`: `./gradlew :frontend:test --tests '*PromptPanelTest'`
|
||||
- From `packages/kilo-jetbrains/`: `./gradlew typecheck`
|
||||
|
||||
## Notes
|
||||
- Do not use IntelliJ internal APIs. The planned APIs are public extension points/classes used by IntelliJ source examples.
|
||||
- Keep paste support frontend-only. It should reuse existing prompt attachment DTOs and send flow.
|
||||
- Preserve normal text paste. The custom provider should be enabled only for file-list or image clipboard content.
|
||||
- Avoid temp files for raw images because `data:` file URLs are already supported by Kilo prompt handling and work better across split-mode boundaries than frontend-local temp paths.
|
||||
@@ -1,82 +0,0 @@
|
||||
# Plan: JetBrains Prompt Attachment Rendering
|
||||
|
||||
## Goal
|
||||
Improve how attachments appear in the JetBrains session transcript prompt view:
|
||||
|
||||
- Attachments render visually inside the user prompt box with the prompt text.
|
||||
- Attachment previews show when possible, including embedded pasted image data URLs.
|
||||
- Clicking an attachment opens its content in IntelliJ as in-memory editor content when the attachment is embedded/non-local; local files keep opening as files.
|
||||
|
||||
## Current Findings
|
||||
|
||||
- Transcript user messages are rendered through `MessageView` in `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt`.
|
||||
- User text parts route to `PromptView`; user file parts route to `AttachmentView` via `ViewFactory.createUser(...)`.
|
||||
- `MessageView` currently treats each part as an independent child view, so user file parts are not grouped with prompt text as a single prompt body.
|
||||
- `AttachmentView` uses `AttachmentCard`, shared with the input prompt attachment strip.
|
||||
- `AttachmentCard` only previews local image files via `file://...`; pasted image data URLs currently fall back to the generic image icon.
|
||||
- Existing local file open flow is `SessionUi.openFile(...) -> KiloWorkspaceService.openPath(...)` for transcript attachments, and direct `FileEditorManager.openFile(...)` for input prompt chips.
|
||||
- No plugin helper currently opens arbitrary in-memory content in editor tabs.
|
||||
- Local IntelliJ source is available at `$INTELLIJ_REPO=/Users/kirillk/products/intellij-community`; source confirms `LightVirtualFile` / `BinaryLightVirtualFile` are in-memory `VirtualFile` implementations and platform code opens them with `FileEditorManager.getInstance(project).openFile(file, true)`.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Add a small frontend-only in-memory opener.
|
||||
- New file under `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/attachment/`, likely `AttachmentOpeners.kt`.
|
||||
- Provide a function that accepts `Project`, attachment name, MIME, and URL.
|
||||
- For `data:*;base64,...` URLs, decode the bytes.
|
||||
- For textual MIME or non-binary data, open `LightVirtualFile(name, fileType, text)`.
|
||||
- For image/PDF/binary MIME, open `BinaryLightVirtualFile(name, fileType, bytes)` so IntelliJ can use native image/PDF/file viewers where supported.
|
||||
- Infer file type with `FileTypeManager.getInstance().getFileTypeByFileName(name)` and fall back to plain text only for text content.
|
||||
- Use `FileEditorManager.getInstance(project).openFile(file, true)` on the EDT.
|
||||
|
||||
2. Thread the new open callback through session transcript rendering.
|
||||
- Add an `openAttachment: (FileAttachment) -> Unit` or equivalent callback to `SessionMessageListPanel`, `TurnView`, `MessageView`, and `ViewFactory.createUser(...)`.
|
||||
- In `SessionUi`, implement the callback:
|
||||
- `file://` URLs keep using `openFile(path)`.
|
||||
- `data:` URLs use the new in-memory opener.
|
||||
- Other URLs keep using `openUrl(url)`.
|
||||
- Keep existing `openFile` and `openUrl` behavior for assistant markdown/tool links.
|
||||
|
||||
3. Group user prompt parts into one prompt box.
|
||||
- Add a retained Swing `PromptMessageView` or similar under `session/views/` that owns a vertical/flow layout for user prompt content.
|
||||
- It should render `Text` with `PromptView` and `FileAttachment` with `AttachmentView`/`AttachmentCard` inside the same visual prompt container.
|
||||
- Prefer placing the grouping at the `MessageView` user-role branch so assistant messages and generic part routing stay unchanged.
|
||||
- Preserve the retained Swing model: update existing child views when parts change, add/remove only the affected child, and avoid rebuilding the whole card on every delta.
|
||||
- Keep `MessageView.sessionViewKind == UserPrompt` so the existing right-indent layout still applies to the whole prompt bubble.
|
||||
|
||||
4. Make attachment previews work for embedded images.
|
||||
- Extend `AttachmentCard.load()` to support `data:image/...;base64,...` in addition to local paths.
|
||||
- Decode in a pooled thread using `Base64.getDecoder()` and `ImageIO.read(ByteArrayInputStream(...))`.
|
||||
- Keep existing generation/displayable guards before applying the preview icon on the EDT.
|
||||
- Leave non-image data URLs as icon-only.
|
||||
|
||||
5. Improve transcript attachment click behavior.
|
||||
- Update `AttachmentView.open(...)` to delegate to the new callback instead of deciding only `file://` versus browser URL itself.
|
||||
- Keep local file opening unchanged.
|
||||
- For embedded pasted images, click opens a binary in-memory virtual file tab named from `filename`.
|
||||
- For embedded text/data, click opens a text in-memory virtual file tab.
|
||||
|
||||
6. Add or update tests.
|
||||
- `SessionUiUpdateTest` or `SessionMessageListPanelTest`: verify a user message with text + file parts creates one user prompt message container and the attachment card is inside that container, not a separate prompt box.
|
||||
- `AttachmentCard` focused test: local images still use previews; data URL images decode and replace the default icon. If direct pixel/icon assertions are brittle, expose only test-safe observable state through existing component tree behavior.
|
||||
- `AttachmentView`/message panel test: clicking a `file://` attachment calls the local file opener, clicking a `data:` attachment calls the in-memory opener/open callback.
|
||||
- Existing tests that assert `AttachmentView#...` still appears may need adjustment if attachments become nested inside a new prompt-group view.
|
||||
|
||||
7. Add release note.
|
||||
- Create a patch changeset under `.changeset/` for `@kilocode/kilo-jetbrains` if the package is listed there; otherwise follow the repo’s changeset convention for JetBrains user-facing fixes.
|
||||
- User-facing wording: `Show JetBrains prompt attachments inside the prompt bubble with previews and open embedded attachments in editor tabs.`
|
||||
|
||||
## Verification
|
||||
|
||||
Run the smallest relevant JetBrains checks after implementation:
|
||||
|
||||
- `./gradlew test --tests "ai.kilocode.client.session.*"` from `packages/kilo-jetbrains/` if Gradle test filtering works for the touched tests.
|
||||
- If targeted filtering is not reliable, run `./gradlew test` from `packages/kilo-jetbrains/`.
|
||||
- Run `./gradlew typecheck` from `packages/kilo-jetbrains/`.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Keep changes inside `packages/kilo-jetbrains/` and `.changeset/`; no shared OpenCode files should be touched.
|
||||
- Use Swing/IntelliJ platform components only; no Compose/JCEF/UI DSL.
|
||||
- Annotate Swing-mutating helpers with `@RequiresEdt` where appropriate.
|
||||
- Do not add RPC for this unless implementation reveals the attachment content is unavailable in the frontend; current DTOs already carry `url`, MIME, and filename.
|
||||
@@ -1,88 +0,0 @@
|
||||
# Plan: JetBrains Prompt Attachment Layout
|
||||
|
||||
## Goal
|
||||
Fix transcript prompt attachment rendering in the JetBrains plugin so user prompt attachments render inside the prompt bubble as one horizontal stack. Multiple attachments should not create repeated raw tool payload blocks. If the attachment row exceeds the prompt width, it should scroll horizontally and never vertically.
|
||||
|
||||
## Current State
|
||||
- `MessageView` renders each user message part independently through `ViewFactory.createUser(...)`.
|
||||
- User `Text` parts become `PromptView` and user `FileAttachment` parts become individual `AttachmentView` children.
|
||||
- Because the parent message uses vertical `SessionLayoutPanel`, multiple attachments render vertically.
|
||||
- `AttachmentView` currently wraps one `AttachmentCard` in `FlowLayout` with prompt-like padding, so it cannot coordinate multiple attachments into a single row.
|
||||
- The current in-progress attachment work already added data image previews and embedded attachment opening. Preserve that behavior.
|
||||
|
||||
## Implementation Steps
|
||||
1. Add a transcript attachment strip view.
|
||||
- Create a new `PromptAttachmentView` or `AttachmentStripView` under `frontend/src/main/kotlin/ai/kilocode/client/session/views/`.
|
||||
- Make it a `PartView` that owns a `LinkedHashMap<String, FileAttachment>` and matching `AttachmentCard` children.
|
||||
- Use `Stack.horizontal(gap = UiStyle.Gap.sm())` or a small custom row panel for card layout.
|
||||
- Wrap the row in `JBScrollPane` configured with horizontal scrollbar as needed and vertical scrollbar never.
|
||||
- Make the scroll pane and viewport transparent, remove the scroll pane border, and keep the strip visually inside the existing user prompt bubble.
|
||||
- Keep the strip height bounded to one attachment-card row plus padding and any horizontal scrollbar height.
|
||||
- Use a stable synthetic `contentId` for the strip, such as `attachments:<messageId>`.
|
||||
- Implement retained updates: add, update, and remove cards without rebuilding the whole message tree when possible.
|
||||
|
||||
2. Change user-message rendering in `MessageView` to aggregate attachments.
|
||||
- Replace the current one-part-one-view handling for user `FileAttachment` parts with one synthetic strip view per user message.
|
||||
- Preserve `parts` lookup semantics for tests and incremental updates. A practical approach is:
|
||||
- keep `parts` for non-attachment part views
|
||||
- keep an `attachments` map for attachment ids
|
||||
- expose `part(id)` for an attachment id by returning the strip view
|
||||
- expose `partIds()` in original visible order, while not adding duplicate strip components
|
||||
- Insert the strip component at the correct location in the component order based on the first visible attachment part.
|
||||
- On new attachment parts, update the existing strip and reposition it if the first attachment order changes.
|
||||
- On attachment removal, remove the card from the strip; when the strip becomes empty, remove and dispose it.
|
||||
- Keep non-user messages using existing `AttachmentView` behavior unless there is a clear reason to aggregate them too.
|
||||
|
||||
3. Suppress raw user-side tool payload noise related to attachment reads.
|
||||
- The screenshots show raw tool input text inside the user prompt bubble. Identify which `Content` subtype represents those blocks in the local model during implementation.
|
||||
- If these blocks are `Tool` parts under user messages and correspond to read/file attachment metadata, filter them from user-message rendering in `MessageView.isHidden(...)` or a user-specific visibility helper.
|
||||
- Keep assistant tool renderers unchanged.
|
||||
- Do not change RPC DTOs, backend payloads, or persisted transcript data; this should be a frontend rendering decision only.
|
||||
|
||||
4. Preserve attachment opening and previews.
|
||||
- Reuse `AttachmentCard` for each card.
|
||||
- Reuse the existing `openAttachment: (FileAttachment) -> Unit` callback threaded from `SessionUi`.
|
||||
- Preserve local file opening, external URL opening, embedded data URL opening, and data image previews.
|
||||
- Keep remove buttons disabled in transcript cards; transcript attachments should open on click only.
|
||||
|
||||
5. Adjust spacing and borders.
|
||||
- Remove or reduce per-attachment outer padding from the old single `AttachmentView` path for user prompts, since the strip owns row padding.
|
||||
- Use existing `UiStyle.Gap` and `SessionUiStyle.View.Prompt` constants.
|
||||
- Avoid new hardcoded colors or dimensions unless added to `SessionUiStyle` as session-specific tokens.
|
||||
- Keep the row inside the existing `MessageView` rounded prompt paint, not in a separate nested bubble.
|
||||
|
||||
## Tests
|
||||
1. Update `SessionUiUpdateTest`.
|
||||
- Add a test where a user message has text plus multiple `FileAttachment` parts.
|
||||
- Assert there is one attachment strip component inside the user `MessageView`.
|
||||
- Assert all attachment cards are descendants of that strip and are not direct vertical siblings as separate `AttachmentView`s.
|
||||
- Assert `partIds()` still includes the original text and attachment ids in stable order.
|
||||
- Assert clicking each attachment card delegates to `openAttachment` with the correct URL.
|
||||
|
||||
2. Add overflow behavior test.
|
||||
- Create enough attachments to exceed a narrow message width.
|
||||
- Layout the component and assert the strip contains a `JBScrollPane` with horizontal scrollbar policy `HORIZONTAL_SCROLLBAR_AS_NEEDED` and vertical policy `VERTICAL_SCROLLBAR_NEVER`.
|
||||
- Assert preferred height does not grow with the number of attachments.
|
||||
|
||||
3. Add raw payload suppression test.
|
||||
- Build a user message with one or more attachment parts and the corresponding noisy read/tool payload content type discovered during implementation.
|
||||
- Assert the rendered user prompt does not include the raw payload text.
|
||||
- Assert assistant read/tool parts still render normally.
|
||||
|
||||
4. Keep existing tests passing.
|
||||
- Update `test user file part renders as attachment view` if needed to expect the new strip view for user prompts.
|
||||
- Keep data image preview tests in `PromptPanelTest` unchanged unless helper traversal needs adjustment.
|
||||
|
||||
## Verification
|
||||
Run the smallest relevant JetBrains checks from `packages/kilo-jetbrains/`:
|
||||
- `./gradlew frontend:test --tests "ai.kilocode.client.session.ui.SessionUiUpdateTest"`
|
||||
- `./gradlew frontend:test --tests "ai.kilocode.client.session.ui.PromptPanelTest"`
|
||||
- `./gradlew typecheck`
|
||||
|
||||
If Gradle task names differ, use the nearest existing frontend test/typecheck tasks from the package.
|
||||
|
||||
## Non-Goals
|
||||
- Do not change shared RPC schemas or backend transcript generation.
|
||||
- Do not change attachment upload/paste behavior in the input prompt unless required by shared component updates.
|
||||
- Do not replace Swing with Compose, JCEF, or Kotlin UI DSL.
|
||||
- Do not alter assistant tool rendering except to protect it from any user-message-specific suppression logic.
|
||||
@@ -1,53 +0,0 @@
|
||||
# Plan: Hide User Read Tool Payload Text
|
||||
|
||||
## Goal
|
||||
Hide the visible `Called the Read tool with the following input: ...` lines from JetBrains user prompt bubbles, including the screenshot case where those lines are mixed with attachment cards. Keep assistant tool rendering unchanged and do not change backend payloads, stored transcript data, or attachment rendering.
|
||||
|
||||
## Current Findings
|
||||
- The previous user-side `Tool` filtering handles `Tool` content, but the screenshot shows the noisy text inside the user prompt markdown area.
|
||||
- `TextView` renders `Text.content` directly through `MdView` in its constructor, `update(...)`, and `appendDelta(...)`.
|
||||
- `PromptView` subclasses `TextView` for user message text, so this is the right frontend-only boundary for user-specific text cleanup.
|
||||
- `MessageView` now aggregates user `FileAttachment` parts in `PromptAttachmentView`; this should remain unchanged.
|
||||
- Assistant messages use `TextView`, `ToolView`, and `ReadToolView`, so a sanitizer only in `PromptView` will not affect assistant output.
|
||||
|
||||
## Implementation Steps
|
||||
1. Add user prompt text sanitization in `PromptView`.
|
||||
- Keep it frontend-only by transforming only the markdown passed to `MdView`.
|
||||
- Preserve the underlying `Text.content` model unchanged.
|
||||
- Add a private raw text buffer in `PromptView` initialized from the constructor `Text.content`.
|
||||
- After superclass construction, immediately replace the initially rendered raw markdown with the sanitized markdown.
|
||||
- Override `update(content)` to reset the raw buffer from the updated `Text` content and render the sanitized result.
|
||||
- Override `appendDelta(delta)` to append to the raw buffer and render the sanitized full prompt text.
|
||||
- This avoids edge cases where the noisy line arrives through streaming deltas or history load.
|
||||
|
||||
2. Implement a narrowly scoped sanitizer.
|
||||
- Remove whole markdown lines matching the generated payload format:
|
||||
`Called the Read tool with the following input: { ... }`
|
||||
- Match case-insensitively for `Read`, but keep the pattern specific to read-tool payload text.
|
||||
- Require the line to contain a path-like input key such as `"filePath"` or `"path"` to avoid stripping arbitrary user prose.
|
||||
- Remove adjacent excess blank lines only enough to avoid large visual gaps; do not otherwise normalize user text.
|
||||
- Do not strip assistant text, tool bodies, or generic transcript content.
|
||||
|
||||
3. Update tests in `SessionUiUpdateTest`.
|
||||
- Add a user message with one text part containing ordinary prompt text plus multiple `Called the Read tool...` lines and file attachment parts.
|
||||
- Assert the rendered `PromptView.markdown()` contains the ordinary prompt text and does not contain `Called the Read tool` or the file path payloads.
|
||||
- Assert `partIds()` still includes the original text and attachment IDs.
|
||||
- Assert the attachment strip still contains the expected cards.
|
||||
- Add a control assertion that assistant `TextView` or assistant `ReadToolView` still renders normally, so the filter is user-prompt-only.
|
||||
|
||||
4. Consider adding a focused `PromptViewTest` only if `SessionUiUpdateTest` becomes too broad.
|
||||
- Prefer updating the existing integration test first because it exercises the actual `MessageView` and `PromptView` path with attachments.
|
||||
|
||||
5. Verification.
|
||||
- Run from `packages/kilo-jetbrains/`:
|
||||
`./gradlew frontend:test --tests "ai.kilocode.client.session.ui.SessionUiUpdateTest"`
|
||||
- Run the existing attachment-related prompt tests:
|
||||
`./gradlew frontend:test --tests "ai.kilocode.client.session.ui.PromptPanelTest"`
|
||||
- Run:
|
||||
`./gradlew typecheck`
|
||||
|
||||
## Non-Goals
|
||||
- Do not change CLI/backend transcript generation.
|
||||
- Do not mutate `SessionModel` text content or persisted history.
|
||||
- Do not hide assistant read tools or assistant tool output.
|
||||
- Do not change the new horizontal attachment strip behavior except as needed by tests.
|
||||
@@ -1,118 +0,0 @@
|
||||
# Plan: Move User Read Payload Sanitization To KiloCliDataParser
|
||||
|
||||
## Goal
|
||||
Centralize the JetBrains cleanup for generated `Called the Read tool with the following input: ...` prompt text inside the CLI parsing layer, with parser-focused tests, and remove the view-level `PromptView` sanitizer. The frontend should receive already-clean user prompt text while assistant text, assistant tool views, backend payloads outside JetBrains DTO parsing, and attachment rendering remain unchanged.
|
||||
|
||||
## Current Findings
|
||||
- `PromptView` currently contains the new view-only sanitizer from the prior implementation. This should be removed so `PromptView` returns to being a styling-only `TextView` subclass.
|
||||
- `KiloCliDataParser.parseMessages(raw)` has role context because it parses `MessageWithPartsDto(info, parts)`. It can sanitize user text parts during history parsing.
|
||||
- `KiloCliDataParser.parseChatEvent(type, data)` currently parses one event at a time and does not know message roles for `message.part.updated` or `message.part.delta` events.
|
||||
- `KiloBackendChatManager.start(...)` is the backend live SSE routing point and currently calls `KiloCliDataParser.parseChatEvent(event.type, event.data)` before emitting parsed `ChatEventDto`s.
|
||||
- `KiloBackendChatManager` can maintain lightweight live message role context from `ChatEventDto.MessageUpdated` events and use a parser-owned normalizer for subsequent part events.
|
||||
- Streaming deltas make stateless per-delta sanitization unsafe: a generated read-tool line may arrive split across multiple `message.part.delta` events. If a partial noisy line was already emitted, a later delta may reveal that it must be removed. A correct backend solution needs per-user-text-part raw/sanitized state and sometimes must emit a replacement `PartUpdated` rather than only sanitized deltas.
|
||||
- Existing parser tests already cover `parseChatEvent`, `parseMessages`, and many DTO edge cases in `KiloCliDataParserTest`, making it the right place for focused sanitization tests.
|
||||
- Existing `SessionUiUpdateTest` has a UI integration test for the screenshot path. After moving sanitization backend-side, that UI test should either be adjusted to feed already-sanitized DTOs or removed in favor of parser/backend-manager tests, since direct `SessionModel.updateContent(...)` bypasses the backend parser.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
1. Remove view-level sanitization from `PromptView`.
|
||||
- Delete the raw buffer, `update(...)`, `appendDelta(...)`, `sync()`, `sanitize(...)`, regexes, and `Content` import added in the previous pass.
|
||||
- Keep `PromptView` as a thin `TextView` subclass that only sets prompt border/font/background and `dumpLabel()`.
|
||||
- This ensures views do not hide or mutate transcript text.
|
||||
|
||||
2. Add parser-owned user prompt text sanitization in `KiloCliDataParser`.
|
||||
- Add a narrowly scoped internal function such as `sanitizeUserPromptText(text: String): String`.
|
||||
- Match only whole lines that start with the generated payload shape, case-insensitive for `Read`:
|
||||
`Called the Read tool with the following input:`
|
||||
- Require a path-like JSON key on that line, such as `"filePath":` or `"path":`, to avoid stripping ordinary user prose.
|
||||
- Collapse only adjacent blank lines introduced by removed payload lines; do not otherwise normalize text.
|
||||
- Keep the function internal/testable in `KiloCliDataParser` rather than in frontend views.
|
||||
|
||||
3. Sanitize history parsing in `parseMessages(raw)`.
|
||||
- Parse `info` first.
|
||||
- When `info.role == "user"`, sanitize only text parts (`PartDto.type == "text"`) before building `MessageWithPartsDto`.
|
||||
- Leave assistant text parts unchanged.
|
||||
- Leave tool/file/attachment parts unchanged.
|
||||
- Prefer adding a small helper like `sanitizePart(part, role)` to avoid duplicating copy logic.
|
||||
|
||||
4. Add a parser-owned live event normalizer for role-aware streaming.
|
||||
- Add a nested or adjacent parser-owned class, for example `KiloCliDataParser.ChatEventNormalizer`.
|
||||
- The normalizer should be the single place that owns:
|
||||
- message role map by message id, populated from `MessageUpdated` events;
|
||||
- raw text buffer by `(messageID, partID)` for user text parts;
|
||||
- last sanitized text by `(messageID, partID)` so streaming can determine what visible change to emit.
|
||||
- API shape can be minimal, for example:
|
||||
`fun parse(type: String, data: String): List<ChatEventDto>`
|
||||
where it internally calls `parseChatEvent(...)` and applies role-aware normalization.
|
||||
- Keep the existing `parseChatEvent(type, data): ChatEventDto?` for stateless tests and other callers, unless all call sites can be moved safely.
|
||||
|
||||
5. Live normalizer behavior.
|
||||
- For `MessageUpdated`, parse normally, store `messageID -> role`, and return the event unchanged.
|
||||
- For `PartUpdated`:
|
||||
- If the message role is `user` and `part.type == "text"`, sanitize `part.text` and store both raw and sanitized text for `(messageID, partID)`.
|
||||
- Return a `PartUpdated` with sanitized text.
|
||||
- For assistant or non-text parts, return unchanged.
|
||||
- For `PartDelta` with `field == "text"`:
|
||||
- If message role is not `user`, return unchanged.
|
||||
- Append the raw delta to the raw buffer for `(messageID, partID)`.
|
||||
- Recompute sanitized full text with `sanitizeUserPromptText(raw)`.
|
||||
- Compare to the last sanitized text for that key.
|
||||
- If the new sanitized text starts with the previous sanitized text, emit a `PartDelta` containing only the appended visible suffix.
|
||||
- If the sanitized text is unchanged, emit no event.
|
||||
- If the sanitized text changes in a non-append way because a previously emitted partial line is now identified as generated payload text, emit a synthetic `PartUpdated` for a text part with the full sanitized text. This allows the frontend model to replace stale rendered text without view hacks.
|
||||
- For `PartRemoved` and `MessageRemoved`, clear associated normalizer state.
|
||||
- On `session.turn.open`, no reset is necessary unless tests reveal stale state; message ids/part ids are unique enough. On `stop()`, the chat manager will discard the normalizer.
|
||||
|
||||
6. Wire the normalizer into `KiloBackendChatManager`.
|
||||
- Add a private parser normalizer field to `KiloBackendChatManager`, initialized with a new instance.
|
||||
- In `start(...)`, replace direct `parseChatEvent(...)` with normalizer parsing.
|
||||
- Emit every returned event in order.
|
||||
- Preserve existing logging for emitted events; if a raw SSE event normalizes to no parsed events because a noisy delta was suppressed, avoid warning as a parse failure.
|
||||
- Reset/recreate normalizer state in `stop()` so reconnects do not keep stale message roles/text buffers.
|
||||
|
||||
7. Update tests in `KiloCliDataParserTest`.
|
||||
- Add direct tests for `sanitizeUserPromptText`:
|
||||
- removes `Called the Read tool...{"filePath":...}` lines;
|
||||
- handles lowercase/uppercase `Read` variants;
|
||||
- handles `"path"` key;
|
||||
- preserves ordinary user prose that merely mentions read tools but lacks the generated payload shape/path key;
|
||||
- collapses adjacent blank lines without broader normalization.
|
||||
- Add `parseMessages` tests:
|
||||
- user text parts are sanitized;
|
||||
- assistant text parts with the same line remain unchanged;
|
||||
- attachments and tool parts remain unchanged.
|
||||
- Add `ChatEventNormalizer` tests:
|
||||
- user `MessageUpdated` then `PartUpdated` sanitizes full text;
|
||||
- assistant `MessageUpdated` then `PartUpdated` preserves text;
|
||||
- user deltas that append normal text emit normal deltas;
|
||||
- user deltas that form a noisy line split across chunks do not leak the final generated payload;
|
||||
- when a partial noisy line was previously emitted and later becomes identifiable as generated payload, the normalizer emits a replacement `PartUpdated` with cleaned text.
|
||||
|
||||
8. Update or relocate UI tests.
|
||||
- Remove the `SessionUiUpdateTest` assertion that expects direct `SessionModel.updateContent(...)` to sanitize text, because direct model mutation bypasses backend parsing by design.
|
||||
- Keep UI tests for attachment strip rendering and user read `Tool` part suppression.
|
||||
- If useful, add a backend/controller-level integration test only if existing test infrastructure can route parsed events through `KiloBackendChatManager`; otherwise rely on parser normalizer tests plus existing frontend tests that verify rendering of whatever text the model receives.
|
||||
|
||||
9. Changeset.
|
||||
- Keep or update `.changeset/jetbrains-hide-read-payloads.md` so release notes still describe the user-visible fix.
|
||||
- No new changeset is needed if this plan is implemented in the same uncommitted change set and the existing changeset already covers the fix.
|
||||
|
||||
## Verification
|
||||
Run from `packages/kilo-jetbrains/`:
|
||||
- `./gradlew backend:test --tests "ai.kilocode.backend.cli.KiloCliDataParserTest"`
|
||||
- `./gradlew backend:test --tests "ai.kilocode.backend.app.KiloBackendChatManagerTest"` if a manager-level test is added
|
||||
- `./gradlew frontend:test --tests "ai.kilocode.client.session.ui.SessionUiUpdateTest"`
|
||||
- `./gradlew frontend:test --tests "ai.kilocode.client.session.ui.PromptPanelTest"`
|
||||
- `./gradlew typecheck`
|
||||
|
||||
## Non-Goals
|
||||
- Do not change CLI/server transcript generation.
|
||||
- Do not change RPC DTO schema.
|
||||
- Do not sanitize assistant text or assistant tool output.
|
||||
- Do not remove user read `Tool` part suppression in `MessageView`; that is separate from embedded text cleanup.
|
||||
- Do not add new frontend rendering hacks.
|
||||
|
||||
## Risks And Notes
|
||||
- The most important edge case is split streaming deltas. A stateless sanitizer on individual `PartDelta.delta` strings is not sufficient.
|
||||
- Synthetic `PartUpdated` replacement events for user text are acceptable because the frontend model already handles `PartUpdated` as full content replacement.
|
||||
- The normalizer should stay small and parser-owned so the parsing/cleanup behavior is tested in one place, while `KiloBackendChatManager` only wires SSE events through it.
|
||||
@@ -1,43 +0,0 @@
|
||||
# Remove Stale Empty Prompt Panels
|
||||
|
||||
## Cause
|
||||
|
||||
The remaining blank areas are not just padding. User prompt messages can contain text parts whose visible content is empty after sanitization or update. Those parts still render as `PromptView` / `TextView` components, so they keep their border padding and participate in `MessageView` layout even though they display no text. In the screenshot this appears as empty panels/bars below the attachment strip.
|
||||
|
||||
Relevant paths:
|
||||
|
||||
- `SessionModel.updateContent()` creates a `Text` content object for every `PartDto(type = "text")`, even when `dto.text` is empty or sanitized to empty.
|
||||
- `SessionModel.updateExisting()` updates existing `Text` content to an empty string and fires `ContentUpdated`, leaving the renderer alive.
|
||||
- `TextView` / `PromptView` still have preferred size and padding when markdown is empty.
|
||||
- `MessageView` has no filter/removal path for text content that becomes empty.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
1. Add a model-level empty text guard in `SessionModel`.
|
||||
- Treat text parts whose `dto.text` is `null`, empty, or blank after sanitization as non-renderable.
|
||||
- For a new empty text part, do not add it to `msg.parts` and do not fire `ContentAdded`.
|
||||
- For an existing text part updated to empty, remove it from `msg.parts`, fire `ContentRemoved(messageId, contentId)`, and update the header.
|
||||
- Keep assistant streaming behavior intact: `appendDelta()` should still create a `Text` when a non-empty delta arrives.
|
||||
- Be conservative for `Reasoning`: only apply this behavior to `Text`, not reasoning/tool/file parts.
|
||||
|
||||
2. Add a view-level safety filter in `MessageView`.
|
||||
- Introduce a small helper such as `visible(content)` or `hidden(content)` extension logic that returns false for `Text` with `content.isBlank()`.
|
||||
- Use it in initial population, `upsertPart()`, `rebuildParts()`, `partIds()`, and `attachmentIndex()` so stale empty text parts are not rendered even if they arrive from older state/history.
|
||||
- If `upsertPart()` receives an empty `Text`, remove/dispose any existing `PartView` for that id and refresh instead of updating it.
|
||||
- Preserve existing file attachment grouping behavior.
|
||||
|
||||
3. Keep the prior attachment spacing fix, but do not add more padding hacks.
|
||||
- `PromptAttachmentView` should remain compact: no extra top padding, standard bottom shell padding, and scrollbar height reserved via actual scroll bar preferred height.
|
||||
- Do not solve empty panels by reducing card height or hiding scrollbars.
|
||||
|
||||
4. Update tests.
|
||||
- Add `SessionModelTest` coverage that `updateContent()` ignores a new empty user text part.
|
||||
- Add `SessionModelTest` coverage that updating an existing text part to empty removes it and emits `ContentRemoved`.
|
||||
- Add `SessionUiUpdateTest` coverage for the screenshot scenario: user text + attachments + an empty/sanitized text part should render only the visible prompt text and one `PromptAttachmentView`, with no extra empty prompt component.
|
||||
- Add `SessionUiUpdateTest` coverage that an existing rendered prompt text part removed/updated to empty disappears from the `MessageView` component tree.
|
||||
- Update any existing tests that assumed `TextView(Text("p1"))` rendering an empty markdown is a useful UI state if needed; keep pure `TextView` unit tests if they are testing component behavior, but prevent empty text from reaching transcript rendering.
|
||||
|
||||
5. Verify.
|
||||
- Run `./gradlew frontend:test --tests ai.kilocode.client.session.model.SessionModelTest` from `packages/kilo-jetbrains/`.
|
||||
- Run `./gradlew frontend:test --tests ai.kilocode.client.session.ui.SessionUiUpdateTest` from `packages/kilo-jetbrains/`.
|
||||
- Run `./gradlew typecheck` from `packages/kilo-jetbrains/`.
|
||||
@@ -1,199 +0,0 @@
|
||||
# JetBrains Attachment VFS Opening
|
||||
|
||||
## Goal
|
||||
|
||||
Improve opening embedded file attachments in the JetBrains session UI by using the Kilo-specific VFS from `glaze-fireplace`. Embedded/session-owned attachments should open as stable Kilo editor tabs that can be deduplicated and found from Recent Files even when the session UI is not open. Real `file:` attachments should keep using the native IntelliJ file opening flow.
|
||||
|
||||
## Current Behavior
|
||||
|
||||
- Session attachment cards call `SessionUi.openAttachment`.
|
||||
- `file:` URLs are resolved through the workspace/backend and opened as real local files.
|
||||
- `data:` URLs are opened through `openEmbeddedAttachment` as transient `LightVirtualFile` / `BinaryLightVirtualFile` instances.
|
||||
- Transient embedded attachment tabs have no stable identity, can duplicate, cannot be reconstructed through Recent Files, and use `testFramework.BinaryLightVirtualFile` in production.
|
||||
|
||||
## Chosen Direction
|
||||
|
||||
Use Option A: route only embedded/session-owned attachments through the Kilo VFS.
|
||||
|
||||
- Keep real local files on the native local-file VFS.
|
||||
- Keep web/remote URLs opening in the browser.
|
||||
- Add a new Kilo VFS editor kind for embedded attachments.
|
||||
- Store stable attachment identity in the Kilo VFS path params, not attachment bytes.
|
||||
|
||||
## Prerequisite Merge
|
||||
|
||||
1. Merge `glaze-fireplace` into the current branch.
|
||||
- Brings `frontend/src/main/kotlin/ai/kilocode/client/vfs/` infrastructure.
|
||||
- Brings XML registrations for `virtualFileSystem` and `fileEditorProvider`.
|
||||
- Brings VFS tests.
|
||||
2. Resolve any conflicts, if present.
|
||||
- Expected risk is low because the branch is additive and kilo-owned.
|
||||
3. Verify merged VFS tests still pass before building the attachment feature.
|
||||
|
||||
## VFS Attachment Path Design
|
||||
|
||||
Add a Kilo editor kind with id `attachment`.
|
||||
|
||||
Encode identity and presentation metadata in `KiloPath.params`:
|
||||
|
||||
- `sessionId`
|
||||
- `messageId`
|
||||
- `partId`
|
||||
- `filename`
|
||||
- `mime`
|
||||
- optionally `directory` if the attachment fetch path cannot reliably infer the workspace from the active project/session service
|
||||
|
||||
Use a single helper to build params in stable key order. The same attachment must serialize to the same Kilo VFS path so opening it twice focuses the existing editor tab instead of creating duplicates.
|
||||
|
||||
Do not encode the `data:` URL or base64 content into the path. Large content in VFS paths would bloat Recent Files state, tab presentation, logs, and equality checks.
|
||||
|
||||
## Attachment Editor Kind
|
||||
|
||||
Implement `AttachmentEditorKind : KiloEditorKind`.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
1. `title(project, params)`
|
||||
- Return `filename` when present.
|
||||
- Fallback to `partId` or a localized generic attachment title.
|
||||
2. `presentablePath(project, params)`
|
||||
- Return a human-readable path such as `Kilo / Attachments / <sessionId> / <filename>`.
|
||||
- Avoid exposing the serialized JSON path directly.
|
||||
3. `icon(project, params)`
|
||||
- Use MIME/file-name based icons, matching existing `AttachmentCard` behavior where practical.
|
||||
4. `isValid(project, params)`
|
||||
- Return false if required params are missing or the project is disposed.
|
||||
- If validation can cheaply confirm the session/part still exists, use that; otherwise let content loading render a clear missing-attachment state.
|
||||
5. `createContent(project, file, parent)`
|
||||
- Create a retained Swing component immediately on the EDT.
|
||||
- Show a loading state first.
|
||||
- Fetch/decode attachment content off the EDT in a coroutine scoped to `parent`.
|
||||
- Switch back to EDT for UI mutations.
|
||||
- Render images through a normal Swing image component, not `BinaryLightVirtualFile`.
|
||||
- Render textual content with an editor-like viewer where practical.
|
||||
- Render unsupported binary content with filename, MIME type, size, and a useful fallback action if available.
|
||||
|
||||
## Attachment Content Fetching
|
||||
|
||||
Preferred implementation: add a focused RPC.
|
||||
|
||||
Shared API:
|
||||
|
||||
- Add `AttachmentDataDto` with `filename`, `mime`, `bytesBase64` or `text`, and possibly `encoding`.
|
||||
- Add `suspend fun attachment(id: String, directory: String, messageId: String, partId: String): AttachmentDataDto?` to `KiloSessionRpcApi`.
|
||||
|
||||
Backend implementation:
|
||||
|
||||
1. Load the session messages using the same source as `messages(id, directory)`.
|
||||
2. Locate `messageId` then `partId` where `type == "file"`.
|
||||
3. Resolve content:
|
||||
- For `data:` URL, parse and decode the data URL.
|
||||
- For `file:` URL, prefer returning a marker that frontend should open the native file path, or return bytes only if this RPC is explicitly limited to embedded attachments.
|
||||
- For remote URLs, do not fetch automatically unless there is already a safe product pattern for doing so.
|
||||
4. Return null or a structured missing/error state if not found.
|
||||
|
||||
Fallback implementation if avoiding new RPC:
|
||||
|
||||
- Use existing `KiloSessionRpcApi.messages(id, directory)` from the attachment editor kind and locate the part client-side.
|
||||
- Decode only `data:` URLs client-side.
|
||||
- This is less efficient for large sessions but reduces backend/API surface.
|
||||
|
||||
Recommendation: use the focused RPC if implementation cost is modest; otherwise start with the existing `messages` RPC and refactor later.
|
||||
|
||||
## Routing Changes
|
||||
|
||||
Update `SessionUi.openAttachment`:
|
||||
|
||||
1. If URL is blank, return.
|
||||
2. If `isEmbeddedAttachment(url)`:
|
||||
- Build VFS params from the current session id, message id, attachment part id, filename, and MIME type.
|
||||
- Call `project.service<KiloVfsManager>().open("attachment", params)` on EDT.
|
||||
- Return.
|
||||
3. If URL scheme is `file`:
|
||||
- Keep existing `openFile(path)` flow.
|
||||
- Real files should remain native IntelliJ files.
|
||||
4. Otherwise:
|
||||
- Keep existing `openUrl(url)` flow.
|
||||
|
||||
Model/UI requirement:
|
||||
|
||||
- `FileAttachment` currently has `id`, `mime`, `url`, and `filename`, but `openAttachment(item: FileAttachment)` may not carry `messageId` directly.
|
||||
- Update the callback wiring so the opener receives enough context, likely `messageId` plus `FileAttachment`.
|
||||
- Keep the change minimal: avoid reshaping all part views if passing context through `PromptAttachmentView` / `AttachmentView` is enough.
|
||||
|
||||
## Existing Embedded Opener Cleanup
|
||||
|
||||
After VFS routing is in place:
|
||||
|
||||
- Remove or stop using `openEmbeddedAttachment` for transcript attachments.
|
||||
- Keep `decodeDataImage` if still used by `AttachmentCard` thumbnails.
|
||||
- Remove production usage of `BinaryLightVirtualFile` if no longer needed.
|
||||
- Keep `AttachmentView.openDefault` behavior in mind for tests or non-`SessionUi` callers; either update it to support VFS where context is available or leave it as a simple fallback with no embedded support.
|
||||
|
||||
## Recent Files Behavior
|
||||
|
||||
Expected behavior after implementation:
|
||||
|
||||
- Opening the same embedded attachment twice reuses/focuses the same tab because the Kilo VFS path is stable.
|
||||
- Closing the session UI does not invalidate the editor tab.
|
||||
- Closing the attachment tab allows it to be reopened from Recent Files during the same IDE launch because IntelliJ can reconstruct the `KiloVirtualFile` from the serialized Kilo path.
|
||||
- The existing `launchId` in `KiloVfsManager` means old tabs should not auto-restore across IDE restarts. This is acceptable for the current goal unless product requirements explicitly demand cross-restart restore.
|
||||
|
||||
If cross-restart Recent Files restore is required later, revisit `launchId` and add a backend/session readiness recovery flow before removing or changing the scoping.
|
||||
|
||||
## Tests
|
||||
|
||||
Add or update tests under `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/`.
|
||||
|
||||
1. VFS identity/routing tests
|
||||
- Opening the same attachment params twice does not duplicate tabs.
|
||||
- The `KiloVirtualFile` name and presentable path come from filename/session metadata.
|
||||
- Missing required params mark the file invalid or render a missing state.
|
||||
2. Attachment editor kind tests
|
||||
- Embedded text attachment loads and renders content.
|
||||
- Embedded image attachment decodes off-EDT and renders an image component.
|
||||
- Invalid/missing attachment renders an error/missing state without throwing.
|
||||
3. Session UI routing tests
|
||||
- `data:` attachment uses `KiloVfsManager.open("attachment", params)`.
|
||||
- `file:` attachment still uses existing `openFile` path.
|
||||
- Web URL still uses `openUrl`.
|
||||
- Params include `sessionId`, `messageId`, `partId`, `filename`, and `mime`.
|
||||
4. RPC tests if adding focused RPC
|
||||
- Finds the correct file part by session/message/part id.
|
||||
- Decodes `data:` content correctly.
|
||||
- Returns null or structured missing result for absent message/part.
|
||||
|
||||
## Verification
|
||||
|
||||
From `packages/kilo-jetbrains/` run the smallest relevant checks:
|
||||
|
||||
1. Targeted frontend tests for VFS and session UI routing.
|
||||
2. Targeted backend/shared RPC tests if a new RPC is added.
|
||||
3. `./gradlew typecheck` or `bun run typecheck`.
|
||||
|
||||
If Java 21 is missing, install/use it via SDKMAN before running Gradle checks as documented in `packages/kilo-jetbrains/AGENTS.md`.
|
||||
|
||||
## Risks And Mitigations
|
||||
|
||||
- Missing `messageId` in the current opener callback.
|
||||
- Mitigation: thread message context through the existing `MessageView` / `PromptAttachmentView` / `AttachmentView` constructors with the smallest callback signature change.
|
||||
- Large attachments causing UI stalls.
|
||||
- Mitigation: decode/fetch off-EDT and update UI only on EDT.
|
||||
- VFS path instability due to unordered params.
|
||||
- Mitigation: centralize param creation with stable ordering.
|
||||
- Cross-restart expectations.
|
||||
- Mitigation: document current same-launch behavior; only change `launchId` semantics if product explicitly requires cross-restart restore.
|
||||
- Adding too much custom binary rendering.
|
||||
- Mitigation: start with image and text support, render other binary types with a simple metadata fallback.
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. Merge `glaze-fireplace`.
|
||||
2. Run existing VFS tests or at least targeted typecheck to confirm the merge.
|
||||
3. Add attachment params helper and `AttachmentEditorKind`.
|
||||
4. Register `AttachmentEditorKind` with `KiloVfsRegistry`.
|
||||
5. Add focused attachment RPC if chosen; otherwise implement using existing `messages` RPC.
|
||||
6. Update `SessionUi.openAttachment` and opener callback wiring to pass message context.
|
||||
7. Remove/stop using transient embedded `BinaryLightVirtualFile` path.
|
||||
8. Add tests.
|
||||
9. Run targeted tests and typecheck.
|
||||
@@ -1,43 +0,0 @@
|
||||
# Plan: Show Kilo VFS Tabs In Recent Files
|
||||
|
||||
## Goal
|
||||
Make files opened through Kilo VFS appear in IntelliJ Recent Files during the current IDE session, without persisting stale session-owned attachment/editor tabs across IDE restarts.
|
||||
|
||||
## Findings
|
||||
- Kilo VFS files are opened with `FileEditorManager.openFile(...)`, but `KiloVirtualFile` currently only implements `VirtualFileWithoutContent` and `VirtualFilePathWrapper`.
|
||||
- IntelliJ `EditorHistoryManager` includes a file only when either:
|
||||
- the file implements `EditorHistoryManager.OptionallyIncluded` and returns true from `isIncludedInEditorHistory(project)`, or
|
||||
- `VirtualFileManager.findFileByUrl(file.url)` can resolve it.
|
||||
- Relying on the fallback VFS URL lookup is brittle for custom `LightVirtualFileBase` plus `NonPhysicalFileSystem` files, especially because Kilo paths depend on a launch id, open project hash, and a Kilo editor-kind registry.
|
||||
- IntelliJ precedents for ephemeral editor tabs explicitly opt into editor history and disable persistence:
|
||||
- `plugins/agent-workbench/chat/src/AgentChatVirtualFile.kt` implements `EditorHistoryManager.IncludeInEditorHistoryFile` and `isPersistedInEditorHistory() = false`.
|
||||
- `SettingsVirtualFile` implements `EditorHistoryManager.OptionallyIncluded`, returns included, and disables persistence.
|
||||
- Kilo already has a `launchId` in `KiloPath`, matching IntelliJ's `ComplexPathVirtualFileSystem` guidance for avoiding stale pointer identity across launches.
|
||||
- `KiloVfsManager.open()` sets `FileEditorProvider.KEY` to a new provider instance. IntelliJ's agent-chat precedent sets this key only in unit-test mode for `TestEditorManagerImpl`; production should rely on the registered provider. This is probably not the direct Recent Files issue, but it is worth aligning for provider/history stability.
|
||||
|
||||
## Implementation Steps
|
||||
1. Update `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/vfs/KiloVirtualFile.kt`:
|
||||
- Import `com.intellij.openapi.fileEditor.impl.EditorHistoryManager`.
|
||||
- Add `EditorHistoryManager.IncludeInEditorHistoryFile` to `KiloVirtualFile`.
|
||||
- Override `isPersistedInEditorHistory(): Boolean = false`.
|
||||
- Keep existing `isValid()`, `equals()`, `hashCode()`, `launchId`, and `REOPEN_WINDOW=false` behavior unchanged.
|
||||
|
||||
2. Update `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/vfs/KiloVfsManager.kt`:
|
||||
- Remove the production `file.putUserData(FileEditorProvider.KEY, KiloFileEditorProvider())` assignment unless tests prove the real editor manager requires it.
|
||||
- Remove the now-unused `FileEditorProvider` import.
|
||||
- Rely on the existing XML-registered `<fileEditorProvider implementation="ai.kilocode.client.vfs.KiloFileEditorProvider"/>`.
|
||||
|
||||
3. Add focused VFS tests:
|
||||
- In `KiloVirtualFileTest`, assert a `KiloVirtualFile` is included in editor history and is not persisted by checking the `EditorHistoryManager.IncludeInEditorHistoryFile`/`OptionallyIncluded` contract.
|
||||
- In `KiloVfsManagerTest`, after opening a test Kilo VFS file and dispatching events, assert `EditorHistoryManager.getInstance(project).fileList` contains the opened `KiloVirtualFile`.
|
||||
- Keep tests against real `FileEditorManager` and real EDT; do not mock recent-file/history services.
|
||||
|
||||
4. Validate behavior with the smallest relevant checks:
|
||||
- Run targeted frontend tests from `packages/kilo-jetbrains/`:
|
||||
- `./gradlew :frontend:test --tests "ai.kilocode.client.vfs.*"`
|
||||
- Run JetBrains typecheck:
|
||||
- `bun run typecheck`
|
||||
|
||||
## Caveats
|
||||
- This plan targets current-session Recent Files visibility. It intentionally does not persist Kilo VFS entries across IDE restarts because attachment/session-owned editors may not be restorable before Kilo services and session data are ready.
|
||||
- If cross-restart Recent Files support is later required, it should be a separate feature with explicit restore semantics and stale-session handling.
|
||||
@@ -1,21 +0,0 @@
|
||||
# Fetch Main And Merge
|
||||
|
||||
## Context
|
||||
- Current branch is `balanced-backpack`, tracking `origin/balanced-backpack`, currently ahead by 1 commit.
|
||||
- The worktree is dirty with existing JetBrains-related modified/deleted/untracked files plus several untracked plan files.
|
||||
- Because these local changes may be user/agent work, preserve them and avoid any destructive git commands.
|
||||
|
||||
## Execution Plan
|
||||
1. Re-check `git status --short --branch` immediately before making changes so the pre-merge state is captured.
|
||||
2. Fetch the latest main ref with `git fetch origin main`.
|
||||
3. Merge the fetched ref into the current branch with `git merge origin/main`.
|
||||
4. If Git refuses to merge because local modifications would be overwritten, stop before changing the worktree further and report the exact blocked files. Do not stash, reset, or checkout user changes without explicit approval.
|
||||
5. If merge conflicts occur, inspect conflict markers and resolve them minimally, preserving existing branch changes unless `origin/main` clearly supersedes them.
|
||||
6. After resolving conflicts, stage only the conflict-resolution files required to complete the merge and run `git merge --continue` to create the merge commit.
|
||||
7. Verify the result with `git status --short --branch` and `git log --oneline --decorate -5`.
|
||||
8. If package files involved in conflicts or automatic merge changes are identifiable, run the smallest relevant check for those touched areas; otherwise state that no targeted code check was applicable.
|
||||
|
||||
## Safety Notes
|
||||
- Do not run `git reset --hard`, `git checkout --`, `git clean`, force-push, or amend commits.
|
||||
- Do not commit unrelated untracked plan files or unrelated local work.
|
||||
- Do not push after the merge unless explicitly requested.
|
||||
@@ -1,183 +0,0 @@
|
||||
# JetBrains Backend-Owned Kilo VFS Plan
|
||||
|
||||
## Goal
|
||||
|
||||
Make Kilo virtual editor files first-class backend-owned IntelliJ `VirtualFile`s so editor tabs, `VirtualFileId`, editor history, and Recent Files all see the same canonical file identity.
|
||||
|
||||
The frontend must not ask the backend to open an `attachment` or any other specific Kilo editor kind. It should pass one generic Kilo virtual-file path string. The backend should resolve/open that path without per-kind methods or per-kind dispatch.
|
||||
|
||||
## Current State
|
||||
|
||||
- Kilo VFS is currently registered only in `frontend/src/main/resources/kilo.jetbrains.frontend.xml`.
|
||||
- `KiloVfsManager.open(kind, params)` creates a frontend `KiloVirtualFile` and calls frontend `FileEditorManager.openFile(...)`.
|
||||
- This works for rendering tabs but leaves backend history/Recent Files/RPC identity unreliable in split mode.
|
||||
- The current uncommitted cache change adds `KiloVfsFileCache` and history-only tests. That was a local workaround for frontend-created files and should be dropped for this backend-owned design.
|
||||
- Keep the committed stable identity work: canonical params, no `launchId`, and deterministic `attachmentKey` remain necessary.
|
||||
|
||||
## Target Architecture
|
||||
|
||||
| Concern | Owner |
|
||||
|---|---|
|
||||
| Kilo VFS protocol/path decoding | shared code loaded by backend and frontend |
|
||||
| Canonical `KiloPath` identity | shared |
|
||||
| Backend `VirtualFile` creation/resolution | backend via shared VFS implementation |
|
||||
| Opening a Kilo virtual file | backend RPC using a generic path string |
|
||||
| Editor history / Recent Files identity | backend/platform |
|
||||
| Swing editor UI and attachment rendering | frontend `FileEditorProvider` / `KiloEditorKind` registry |
|
||||
| Attachment-specific params | frontend code that builds the path before RPC |
|
||||
|
||||
Desired open flow:
|
||||
|
||||
```text
|
||||
frontend click
|
||||
-> build canonical Kilo virtual-file path string
|
||||
-> RPC openVirtualFile(path)
|
||||
-> backend resolves path to backend KiloVirtualFile
|
||||
-> backend opens/navigates VirtualFile in platform editor model
|
||||
-> frontend editor UI appears via platform split-mode synchronization
|
||||
-> Recent Files tracks the backend-originated VirtualFile
|
||||
```
|
||||
|
||||
## Design Details
|
||||
|
||||
### Generic Path Contract
|
||||
|
||||
- Add a generic RPC method such as `KiloWorkspaceRpcApi.openVirtualFile(path: String): Boolean`.
|
||||
- The payload is only the virtual-file path string, not `kind`, `params`, or attachment-specific fields.
|
||||
- Keep existing `openFile(path: String)` for real local files. Do not overload it with Kilo paths because its current normalization strips query/fragment-like text and is local-file-specific.
|
||||
- The Kilo path format should continue to encode `KiloPath(projectHash, kind, params)` with canonical params.
|
||||
- Backend should treat `projectHash` as a hint, not as the only project lookup key. In split mode the frontend project hash may not equal the backend project hash.
|
||||
- Backend project resolution order for a Kilo path:
|
||||
1. open project whose `locationHash` matches `KiloPath.projectHash`, if any;
|
||||
2. open project whose `basePath` contains or matches `params["directory"]`, when present;
|
||||
3. first non-default open project as a final fallback.
|
||||
- After resolving the backend project, backend should canonicalize the file path using the backend project hash before opening. This makes the backend-created `VirtualFile` the authoritative identity.
|
||||
|
||||
### Shared VFS Core
|
||||
|
||||
- Move the generic, non-UI VFS pieces into `shared` so both backend and frontend load the same protocol implementation:
|
||||
- `KiloPath`
|
||||
- canonical param helpers
|
||||
- Kilo path encode/decode helpers
|
||||
- generic `KiloVirtualFileSystem`
|
||||
- generic `KiloVirtualFile`
|
||||
- Preserve existing package names where practical to avoid a broad package rename.
|
||||
- Remove dependencies from these generic classes on frontend-only `KiloVfsRegistry` or `KiloEditorKind`.
|
||||
- Backend `KiloVirtualFile` validity should be generic: valid when the project is not disposed and the path decodes/canonicalizes. It should not know whether `kind == "attachment"` or any other kind exists.
|
||||
- Generic backend presentation can use stable params only, for example `filename`, `name`, or `kind`. The frontend editor can still use `KiloEditorKind.title(...)` for the actual editor tab name.
|
||||
|
||||
### Module Registrations
|
||||
|
||||
- Register the same Kilo VFS implementation in backend XML:
|
||||
- `packages/kilo-jetbrains/backend/src/main/resources/kilo.jetbrains.backend.xml`
|
||||
- `<virtualFileSystem key="kilo" implementationClass="...KiloVirtualFileSystem"/>`
|
||||
- Keep frontend VFS registration so frontend deserialization/path lookup can resolve backend-originated files.
|
||||
- Keep `KiloFileEditorProvider` registered only in frontend XML, because it creates Swing UI.
|
||||
- Keep `plugin.xml` module content unchanged unless module descriptor dependencies need a corresponding update.
|
||||
|
||||
### Backend Opening
|
||||
|
||||
- Implement backend `openVirtualFile(path)` in `KiloWorkspaceRpcApiImpl`.
|
||||
- Decode and validate that the path belongs to Kilo VFS.
|
||||
- Resolve backend project using the generic lookup rules above.
|
||||
- Resolve/create the backend `KiloVirtualFile` through `KiloVirtualFileSystem`.
|
||||
- Navigate it using the existing backend `navigate(project, file)` helper based on `OpenFileDescriptor(project, file).navigate(true)`.
|
||||
- Do not dispatch on `KiloPath.kind`; backend should only parse the generic path structure.
|
||||
- If split-mode testing shows backend navigation does not consistently open the visible frontend editor, add a second generic fallback method that returns `VirtualFileId` for the backend-originated file and let the frontend open `id.virtualFile()` on EDT. This uses experimental IntelliJ API and should stay isolated behind the Kilo VFS boundary.
|
||||
|
||||
### Frontend Opening
|
||||
|
||||
- Change `KiloVfsManager` from a frontend-local file opener into a generic RPC bridge.
|
||||
- It should build/accept a Kilo virtual-file path string and call `KiloWorkspaceRpcApi.openVirtualFile(path)` from a coroutine, not on EDT.
|
||||
- It may keep a convenience `open(kind, params)` wrapper only if it simply encodes `KiloPath` and delegates to the path-based method. The backend API remains path-only.
|
||||
- Update `SessionUi.openAttachment(...)` to use this backend-backed generic open path.
|
||||
- Keep attachment-specific logic limited to `attachmentParams(...)`; the backend receives only the encoded Kilo virtual-file path.
|
||||
|
||||
### Frontend Editor Provider
|
||||
|
||||
- Keep `KiloEditorKind`, `KiloVfsRegistry`, `KiloFileEditorProvider`, and `KiloFileEditor` in frontend.
|
||||
- Update `KiloFileEditorProvider.accept(...)` to accept Kilo protocol files by decoded path/kind, not by assuming the file was manually created on the frontend.
|
||||
- `KiloFileEditorProvider.createEditor(...)` can still require a frontend-resolved `KiloVirtualFile` if the shared frontend VFS creates that class from the backend path.
|
||||
- `AttachmentEditorKind.createContent(...)` remains frontend-only and loads attachment content using existing session RPC/services.
|
||||
|
||||
## Drop Current Uncommitted Work
|
||||
|
||||
Before implementing the backend-owned design, remove the cache workaround from the working tree:
|
||||
|
||||
- Delete `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/vfs/KiloVfsFileCache.kt`.
|
||||
- Revert the uncommitted `KiloVirtualFileSystem.findOrCreateFile(...)` change that routes through `KiloVfsFileCache`.
|
||||
- Remove uncommitted tests that only assert closed frontend-created files remain in `EditorHistoryManager.fileList`:
|
||||
- `KiloVfsManagerTest.testClosedDistinctPathsRemainInHistory`
|
||||
- `AttachmentEditorKindTest.testClosedDuplicatePartAttachmentsRemainInHistory`
|
||||
- `KiloVirtualFileSystemTest.testFindFileByPathReusesCanonicalFileInstance`, unless shared/backend VFS still intentionally caches instances.
|
||||
- Do not drop committed stable-key or `attachmentKey` changes.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Clean the workaround.
|
||||
- Remove the uncommitted cache file and cache-only tests listed above.
|
||||
- Ensure the working tree only contains changes needed for backend-owned VFS.
|
||||
|
||||
2. Extract shared VFS core.
|
||||
- Move/copy generic `KiloPath` encoding and canonicalization into `shared`.
|
||||
- Move/refactor `KiloVirtualFileSystem` and `KiloVirtualFile` into shared-compatible code with no frontend registry dependency.
|
||||
- Keep generic `KiloVirtualFile` path equality stable and based on backend project + canonical path.
|
||||
|
||||
3. Rewire frontend VFS UI code.
|
||||
- Adjust imports after the shared extraction.
|
||||
- Keep `KiloEditorKind` and registry frontend-only.
|
||||
- Make the editor provider decode/check Kilo path kind via shared path helpers.
|
||||
- Keep the actual editor UI creation unchanged.
|
||||
|
||||
4. Register backend VFS.
|
||||
- Add the Kilo VFS extension to `kilo.jetbrains.backend.xml`.
|
||||
- Add any required backend Gradle IntelliJ module dependency only if compilation requires it.
|
||||
|
||||
5. Add generic backend RPC opening.
|
||||
- Add `openVirtualFile(path: String): Boolean` to `KiloWorkspaceRpcApi`.
|
||||
- Implement it in `KiloWorkspaceRpcApiImpl` using shared Kilo path decode/project resolution/backend VFS lookup/existing `navigate(...)`.
|
||||
- Update `FakeWorkspaceRpcApi` for frontend tests.
|
||||
|
||||
6. Change frontend attachment opening.
|
||||
- Update `KiloVfsManager` to call `openVirtualFile(path)` via durable RPC from a coroutine.
|
||||
- Update `SessionUi.openAttachment(...)` to call the backend-backed path opener.
|
||||
- Ensure no RPC call runs on EDT.
|
||||
|
||||
7. Add tests for the new contract.
|
||||
- Shared/frontend path tests: canonical param order and stable attachment `attachmentKey` remain unchanged.
|
||||
- Frontend service test: attachment opening sends exactly one generic virtual-file path string to RPC and does not call RPC on EDT.
|
||||
- Backend VFS test: decoding the frontend-built path resolves a backend `KiloVirtualFile` with backend project identity and canonical params.
|
||||
- Backend open test: `openVirtualFile(path)` opens a Kilo `VirtualFile` through the real editor manager in monolith test mode with a test file editor provider.
|
||||
- Recent Files regression: after backend `openVirtualFile(path)` opens two attachment-like paths, `EditorHistoryManager.fileList` contains two distinct Kilo files by canonical path/`attachmentKey`; after closing tabs, history still contains them. This is the backend-owned equivalent of the current failing behavior.
|
||||
|
||||
8. Split-mode/manual verification.
|
||||
- Run the plugin in split mode and click two embedded attachments with the same filename/part id but different URLs.
|
||||
- Verify both tabs open, both appear in Recent Files while open, and both remain in Recent Files after closing tabs.
|
||||
- If tabs do not appear from backend `navigate(...)`, implement the generic `VirtualFileId` fallback described above and repeat the same verification.
|
||||
|
||||
## Verification Commands
|
||||
|
||||
From `packages/kilo-jetbrains/`:
|
||||
|
||||
```bash
|
||||
./gradlew :frontend:test --tests ai.kilocode.client.vfs.KiloVirtualFileSystemTest --tests ai.kilocode.client.session.ui.attachment.AttachmentEditorKindTest
|
||||
./gradlew :backend:test --tests '*Kilo*Virtual*' --tests '*KiloWorkspaceRpcApiImpl*'
|
||||
./gradlew typecheck
|
||||
```
|
||||
|
||||
Also run the JetBrains DevKit frontend/backend API usage inspection for the touched files, because this change intentionally crosses split-mode boundaries.
|
||||
|
||||
## Expected Result
|
||||
|
||||
- Backend owns canonical Kilo `VirtualFile` identity.
|
||||
- Frontend passes only a generic virtual-file path string to backend.
|
||||
- Backend does not expose attachment-specific or kind-specific open methods.
|
||||
- Frontend still owns Kilo editor UI rendering.
|
||||
- Recent Files and editor history track backend-originated Kilo files consistently.
|
||||
- The cache workaround is gone unless a later test proves frontend deserialization still needs a small identity cache.
|
||||
|
||||
## Risks
|
||||
|
||||
- `VirtualFileId` fallback, if needed, uses experimental IntelliJ APIs. Keep it isolated and only use it if backend navigation does not open the visible frontend tab reliably.
|
||||
- Generic backend presentation may be less rich than frontend presentation. That is acceptable initially; identity correctness matters first. Add path/name polish only if Recent Files rows are visually ambiguous after identity is fixed.
|
||||
- Moving VFS core into shared can expose package awkwardness if package names are preserved. Prefer the smallest import churn now over a broad package rename.
|
||||
@@ -1,443 +0,0 @@
|
||||
# JetBrains Kilo VFS — backend-owned identity, frontend-rendered editor (PRESCRIPTIVE)
|
||||
|
||||
Execution plan for a fast model. Supersedes `jetbrains-kilo-vfs-empty-editor-handoff.md`. Implementing this **reverts/deletes** most of the currently-uncommitted VFS code (backend placeholder provider, `adopt`, reopen listener, `openVirtualFile` RPC).
|
||||
|
||||
## Rules for the implementer
|
||||
|
||||
- Work only inside `packages/kilo-jetbrains/`. All target files are Kilo-owned (`ai/kilocode/...`) — **do NOT add `kilocode_change` markers**.
|
||||
- Repo style: single-word names (`path`, `file`, `kind`, `dir`, `cfg`), early returns, no `else`, no empty `catch` (log via the existing logger). Prefer `val`. Don't reformat untouched lines.
|
||||
- Java 21 required for Gradle. Verify `java -version` first; if not 21, `sdk install java 21-tem && sdk use java 21-tem`.
|
||||
- All `FileEditorManager` / `EditorHistoryManager` calls run on EDT.
|
||||
- Use `$INTELLIJ_REPO` (`/Users/kirillk/products/intellij-community`) to confirm any API you are unsure of (e.g. the project-closing topic in Step 12).
|
||||
- Do the steps in order. Build/typecheck after Phase 1, then after Phase 2.
|
||||
|
||||
## Why (condensed root cause, verified in `$INTELLIJ_REPO`)
|
||||
|
||||
- A custom **Swing** `FileEditor` over a non-physical VFS file can only render **client-side**; content is not transported backend→frontend. The platform pattern (reworked Terminal: `plugins/terminal/frontend/...`) registers the provider **frontend-only**, with **no backend provider/placeholder**, and opens via `FileEditorManager.openFile` (not backend `navigate()`).
|
||||
- Backend `openFile` delegates to the client manager when the active `ClientId` isn't local (`FileEditorManagerImpl.kt:1015-1021`), so a **frontend** open bypasses the backend `canNavigate` gate that the placeholder was added to satisfy (`FileNavigatorImpl.kt:21-33`).
|
||||
- **Startup crash** (`Unknown Kilo editor kind: attachment`): the kind is registered from a `postStartupActivity`, but editor restore runs **before** post-startup (`ProjectManagerImpl.kt:784-786` vs `:844-848`) and restore can call `createEditor` **without** `accept` (by saved `editor-type-id`, `EditorCompositeModelManager.kt:102-104`). Fix = register the kind **synchronously inside `accept` and `createEditor`**.
|
||||
- **Blink + duplicate recents**: every open round-trips through backend `navigate()` → empty `JPanel` placeholder (name = raw JSON) → frontend editor → cleanup, and the backend `navigate` records a duplicate `EditorHistoryManager` entry the frontend cleanup never removes. Remove the backend leg → both gone.
|
||||
- **Recents/restart**: backend owns persisted recents/open-editors; the client skips restore unless a registry key is set; the backend can't render our editor → split-mode auto-reopen must be **Kilo-driven** (Phase 2).
|
||||
|
||||
---
|
||||
|
||||
# PHASE 1 — Architecture fix (crash, blink, duplicate recents)
|
||||
|
||||
### Step 1 — `frontend/.../vfs/KiloFileEditorProvider.kt` (register kind eagerly; pure `path()`)
|
||||
|
||||
Replace the whole file with:
|
||||
|
||||
```kotlin
|
||||
package ai.kilocode.client.vfs
|
||||
|
||||
import ai.kilocode.client.session.ui.attachment.ensureAttachmentEditorKind
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.fileEditor.FileEditor
|
||||
import com.intellij.openapi.fileEditor.FileEditorPolicy
|
||||
import com.intellij.openapi.fileEditor.FileEditorProvider
|
||||
import com.intellij.openapi.project.DumbAware
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
|
||||
class KiloFileEditorProvider : FileEditorProvider, DumbAware {
|
||||
override fun accept(project: Project, file: VirtualFile): Boolean {
|
||||
ensureAttachmentEditorKind()
|
||||
val path = path(file) ?: return false
|
||||
return service<KiloVfsRegistry>().get(path.kind) != null
|
||||
}
|
||||
|
||||
override fun acceptRequiresReadAction(): Boolean = false
|
||||
|
||||
override fun createEditor(project: Project, file: VirtualFile): FileEditor {
|
||||
ensureAttachmentEditorKind()
|
||||
val path = path(file) ?: error("Invalid Kilo virtual file: ${file.path}")
|
||||
val kilo = file as? KiloVirtualFile ?: KiloVirtualFile(project, path.copy(projectHash = project.locationHash))
|
||||
val kind = service<KiloVfsRegistry>().get(kilo.path.kind) ?: error("Unknown Kilo editor kind: ${kilo.path.kind}")
|
||||
return KiloFileEditor(project, file, kilo, kind)
|
||||
}
|
||||
|
||||
override fun disposeEditor(editor: FileEditor) {
|
||||
Disposer.dispose(editor)
|
||||
}
|
||||
|
||||
override fun getEditorTypeId(): String = EDITOR_TYPE_ID
|
||||
|
||||
override fun getPolicy(): FileEditorPolicy = FileEditorPolicy.HIDE_OTHER_EDITORS
|
||||
|
||||
companion object {
|
||||
const val EDITOR_TYPE_ID = "KiloVfsEditor"
|
||||
|
||||
private fun path(file: VirtualFile): KiloPath? {
|
||||
if (file is KiloVirtualFile) return file.path
|
||||
if (file.fileSystem.protocol != KiloVirtualFileSystem.PROTOCOL && !file.url.startsWith("${KiloVirtualFileSystem.PROTOCOL}://")) return null
|
||||
return KiloVirtualFileSystem.decode(file.path) ?: KiloVirtualFileSystem.decode(file.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Changes vs current: `ensureAttachmentEditorKind()` is the first line of both `accept` and `createEditor`; the `if (path?.kind == "attachment") ensureAttachmentEditorKind()` side-effect is removed from `path()`. `KiloVfsRegistry` stays as-is.
|
||||
|
||||
### Step 2 — `frontend/.../session/ui/attachment/AttachmentEditorKind.kt` (drop the activity)
|
||||
|
||||
- Delete `AttachmentEditorKindActivity` (the `internal class … : ProjectActivity { … }` block, ~lines 169-173).
|
||||
- Remove the now-unused `import com.intellij.openapi.startup.ProjectActivity`.
|
||||
- Keep `AttachmentEditorKind` as an `object`; keep `ensureAttachmentEditorKind()` exactly as is (`service<KiloVfsRegistry>().register(AttachmentEditorKind)`). `SessionUi.kt:187` still calls it — leave that call.
|
||||
|
||||
### Step 3 — `frontend/.../vfs/KiloVfsManager.kt` (frontend-only open; delete band-aids)
|
||||
|
||||
Replace the whole file with (keep `cs` — Phase 2 uses it):
|
||||
|
||||
```kotlin
|
||||
package ai.kilocode.client.vfs
|
||||
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
||||
import com.intellij.openapi.fileEditor.FileEditorProvider
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@Service(Service.Level.PROJECT)
|
||||
class KiloVfsManager(
|
||||
private val project: Project,
|
||||
private val cs: CoroutineScope,
|
||||
) {
|
||||
@RequiresEdt
|
||||
fun open(kind: String, params: Map<String, String> = emptyMap()) {
|
||||
openLocal(kind, params, focus = true)
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun openLocal(kind: String, params: Map<String, String> = emptyMap(), focus: Boolean = true): Boolean {
|
||||
val file = file(kind, params) ?: return false
|
||||
if (ApplicationManager.getApplication().isUnitTestMode) {
|
||||
file.putUserData(FileEditorProvider.KEY, KiloFileEditorProvider())
|
||||
}
|
||||
FileEditorManager.getInstance(project).openFile(file, focus)
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun close(kind: String, params: Map<String, String> = emptyMap()) {
|
||||
val file = file(kind, params) ?: return
|
||||
FileEditorManager.getInstance(project).closeFile(file)
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun updatePresentation(kind: String, params: Map<String, String> = emptyMap()) {
|
||||
val file = file(kind, params) ?: return
|
||||
FileEditorManager.getInstance(project).updateFilePresentation(file)
|
||||
}
|
||||
|
||||
private fun file(kind: String, params: Map<String, String>): KiloVirtualFile? {
|
||||
return KiloVirtualFileSystem.getInstance().refreshAndFindFileByPath(path(kind, params)) as? KiloVirtualFile
|
||||
}
|
||||
|
||||
private fun path(kind: String, params: Map<String, String>): String {
|
||||
return KiloVirtualFileSystem.getInstance().getPath(KiloPath(project.locationHash, kind, params))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Deleted: `open(path: String)` (RPC), `openLocal(path: String, …)`, `cleanup(...)`, `adopt(...)`, `VirtualFile.isWrapperFor(...)`, and the `KiloWorkspaceService` / `EditorHistoryManager` / `VirtualFile` / coroutine imports they used.
|
||||
|
||||
`SessionUi.openAttachment` (line 596) calls `open(kind, params)` from an EDT click handler — no change needed.
|
||||
|
||||
### Step 4 — Delete `frontend/.../vfs/KiloVfsReopenListener.kt`
|
||||
|
||||
Remove the file. (Phase 2 adds a different, legitimate listener.)
|
||||
|
||||
### Step 5 — `frontend/src/main/resources/kilo.jetbrains.frontend.xml`
|
||||
|
||||
- Remove the attachment post-startup activity line:
|
||||
`<postStartupActivity implementation="ai.kilocode.client.session.ui.attachment.AttachmentEditorKindActivity"/>`
|
||||
- Remove the entire `<projectListeners>` block that registers `KiloVfsReopenListener`.
|
||||
(Both are re-added with new classes in Phase 2 — Step 13.)
|
||||
- Leave `<virtualFileSystem key="kilo" …>` and `<fileEditorProvider id="KiloVfsEditor" …>` unchanged.
|
||||
|
||||
### Step 6 — Delete the backend placeholder
|
||||
|
||||
- Delete `backend/src/main/kotlin/ai/kilocode/backend/vfs/KiloBackendFileEditorProvider.kt`.
|
||||
- In `backend/src/main/resources/kilo.jetbrains.backend.xml`, remove:
|
||||
`<fileEditorProvider id="KiloVfsEditorBackend" implementation="ai.kilocode.backend.vfs.KiloBackendFileEditorProvider"/>`
|
||||
**Keep** `<virtualFileSystem key="kilo" implementationClass="ai.kilocode.client.vfs.KiloVirtualFileSystem"/>` (lets the backend resolve `kilo://` urls so recents show a proper name; the backend still never renders a kilo editor).
|
||||
|
||||
### Step 7 — Remove `openVirtualFile` RPC (frontend → backend navigate)
|
||||
|
||||
- `shared/.../rpc/KiloWorkspaceRpcApi.kt`: delete the `openVirtualFile` declaration and its doc comment (~lines 52-53).
|
||||
- `backend/.../rpc/KiloWorkspaceRpcApiImpl.kt`: delete `override suspend fun openVirtualFile(...)` (lines 190-199) **and** the now-unused private `project(path: KiloPath)` (lines 289-300). Remove the now-unused imports `ai.kilocode.client.vfs.KiloPath` and `ai.kilocode.client.vfs.KiloVirtualFileSystem`.
|
||||
- `frontend/.../app/KiloWorkspaceService.kt`: delete `openVirtualPath(...)` (lines 139-146).
|
||||
|
||||
### Step 8 — `shared/.../vfs/KiloVirtualFile.kt` (history flag)
|
||||
|
||||
- Delete the override `override fun isIncludedInEditorHistory(project: Project): Boolean = !AppMode.isRemoteDevHost()` (line 45) so it inherits the `IncludeInEditorHistoryFile` default (`true`).
|
||||
- Remove `import com.intellij.idea.AppMode`.
|
||||
- Keep `isPersistedInEditorHistory(): Boolean = false`.
|
||||
|
||||
### Phase 1 test edits
|
||||
|
||||
- `frontend/.../testing/FakeWorkspaceRpcApi.kt`: delete `override suspend fun openVirtualFile(...)` and the `val virtualOpened` field. (Phase 2 re-adds two methods + fields — Step 14.)
|
||||
- `frontend/.../app/KiloWorkspaceServiceTest.kt`: delete `` `test openVirtualPath opens virtual file directly` ``.
|
||||
- `frontend/.../vfs/KiloVfsManagerTest.kt`: delete the backend/wrapper/adopt tests — `testOpenUsesBackendVirtualFileRpc`, `testOpenClosesMatchingWrapperAfterFrontendHandoff`, `testAdoptConvertsReopenedWrapperIntoRealEditor`, `testAdoptRealKiloFileIsNoop`, `testAdoptWrapperClosesItWhenRealFileAlreadyOpen`. Keep `testOpenUsesRealFileEditorManager`, `testOpeningSamePathDoesNotDuplicate`, `testOpeningSameStableParamsInDifferentOrderDoesNotDuplicate`, `testDistinctPathsCreateDistinctHistoryEntries`, `testCloseDisposesKindDisposable`. Keep the `scope` / `rpc` / `waitFor` / `RemoteKiloFile` setup — Phase 2's sync test (Step 14) needs `scope`/`rpc`/`waitFor`; `RemoteKiloFile` may be deleted if nothing references it after Phase 2.
|
||||
- `backend/.../rpc/KiloVirtualFileSystemBackendTest.kt`: delete `` `backend editor type id is distinct from frontend` `` and `import ai.kilocode.backend.vfs.KiloBackendFileEditorProvider`. Keep the two decode tests.
|
||||
- Unchanged (verify they still pass): `KiloFileEditorProviderTest` (incl. `testRestoredAttachmentFileRegistersKindBeforeCreateEditor` — `accept` now ensures the kind), `AttachmentEditorKindTest`, `KiloVirtualFileTest` (asserts `isIncludedInEditorHistory` true — still true by default), `KiloVirtualFileSystemTest`, `KiloVfsTestBase` (registry unchanged).
|
||||
|
||||
### Phase 1 build gate (from `packages/kilo-jetbrains/`)
|
||||
|
||||
1. `./gradlew :frontend:test --tests "ai.kilocode.client.vfs.*" --tests "ai.kilocode.client.session.ui.attachment.AttachmentEditorKindTest" --tests "ai.kilocode.client.app.KiloWorkspaceServiceTest"`
|
||||
2. `./gradlew :backend:test --tests "ai.kilocode.backend.*"`
|
||||
3. `./gradlew typecheck`
|
||||
|
||||
Must be green before Phase 2.
|
||||
|
||||
---
|
||||
|
||||
# PHASE 2 — Auto-reopen across restart (backend = source of truth)
|
||||
|
||||
Model: backend persists the open kilo path set (host workspace.xml); the frontend pushes the current set on open/close and reopens it on project startup.
|
||||
|
||||
### Step 9 — Backend store `backend/.../vfs/KiloVfsOpenStore.kt` (NEW)
|
||||
|
||||
Light project service (no XML needed):
|
||||
|
||||
```kotlin
|
||||
package ai.kilocode.backend.vfs
|
||||
|
||||
import com.intellij.openapi.components.PersistentStateComponent
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.State
|
||||
import com.intellij.openapi.components.Storage
|
||||
import com.intellij.openapi.components.StoragePathMacros
|
||||
|
||||
@Service(Service.Level.PROJECT)
|
||||
@State(name = "KiloVfsOpenFiles", storages = [Storage(StoragePathMacros.WORKSPACE_FILE)])
|
||||
class KiloVfsOpenStore : PersistentStateComponent<KiloVfsOpenStore.State> {
|
||||
data class State(var paths: MutableList<String> = mutableListOf())
|
||||
|
||||
private var state = State()
|
||||
|
||||
override fun getState(): State = state
|
||||
|
||||
override fun loadState(state: State) {
|
||||
this.state = state
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun replace(paths: List<String>) {
|
||||
state = State(paths.toMutableList())
|
||||
}
|
||||
|
||||
fun paths(): List<String> = state.paths.toList()
|
||||
}
|
||||
```
|
||||
|
||||
### Step 10 — RPC methods
|
||||
|
||||
`shared/.../rpc/KiloWorkspaceRpcApi.kt` — add:
|
||||
|
||||
```kotlin
|
||||
/** Replace the persisted set of open Kilo virtual paths for [directory]. */
|
||||
suspend fun setVirtualOpenPaths(directory: String, paths: List<String>)
|
||||
|
||||
/** The persisted set of open Kilo virtual paths for [directory]. */
|
||||
suspend fun virtualOpenPaths(directory: String): List<String>
|
||||
```
|
||||
|
||||
`backend/.../rpc/KiloWorkspaceRpcApiImpl.kt` — add (uses the existing `project(path: Path)` + `file(String)` helpers and the already-imported `com.intellij.openapi.components.service`):
|
||||
|
||||
```kotlin
|
||||
override suspend fun setVirtualOpenPaths(directory: String, paths: List<String>) {
|
||||
val project = project(file(directory) ?: return) ?: return
|
||||
project.service<KiloVfsOpenStore>().replace(paths)
|
||||
}
|
||||
|
||||
override suspend fun virtualOpenPaths(directory: String): List<String> {
|
||||
val project = project(file(directory) ?: return emptyList()) ?: return emptyList()
|
||||
return project.service<KiloVfsOpenStore>().paths()
|
||||
}
|
||||
```
|
||||
|
||||
Add `import ai.kilocode.backend.vfs.KiloVfsOpenStore`.
|
||||
|
||||
### Step 11 — Frontend service wrappers `frontend/.../app/KiloWorkspaceService.kt`
|
||||
|
||||
Add (mirror the existing `try/catch` + `LOG.warn` style):
|
||||
|
||||
```kotlin
|
||||
fun setVirtualOpenPaths(directory: String, paths: List<String>) {
|
||||
cs.launch {
|
||||
try {
|
||||
call { setVirtualOpenPaths(directory, paths) }
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("set virtual open paths failed for directory=$directory", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun virtualOpenPaths(directory: String): List<String> {
|
||||
return try {
|
||||
call { virtualOpenPaths(directory) }
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("virtual open paths lookup failed for directory=$directory", e)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 12 — Frontend `KiloVfsManager.sync()` + new listener + restore activity
|
||||
|
||||
Add to `KiloVfsManager` (re-add imports `com.intellij.openapi.components.service`, `ai.kilocode.client.app.KiloWorkspaceService`, `kotlinx.coroutines.launch`):
|
||||
|
||||
```kotlin
|
||||
@RequiresEdt
|
||||
fun sync() {
|
||||
val fs = KiloVirtualFileSystem.getInstance()
|
||||
val paths = FileEditorManager.getInstance(project).openFiles
|
||||
.filterIsInstance<KiloVirtualFile>()
|
||||
.map { fs.getPath(it.path) }
|
||||
val base = project.basePath ?: return
|
||||
cs.launch {
|
||||
val dir = service<KiloWorkspaceService>().resolveProjectDirectory(base)
|
||||
service<KiloWorkspaceService>().setVirtualOpenPaths(dir, paths)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
New `frontend/.../vfs/KiloVfsOpenTracker.kt` — pushes the set on open/close, suppressed during project close so shutdown doesn't wipe the store:
|
||||
|
||||
```kotlin
|
||||
package ai.kilocode.client.vfs
|
||||
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
||||
import com.intellij.openapi.fileEditor.FileEditorManagerListener
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.ProjectCloseListener
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
|
||||
class KiloVfsOpenTracker(private val project: Project) : FileEditorManagerListener {
|
||||
@Volatile private var closing = false
|
||||
|
||||
init {
|
||||
project.messageBus.connect().subscribe(ProjectCloseListener.TOPIC, object : ProjectCloseListener {
|
||||
override fun projectClosing(p: Project) { if (p === project) closing = true }
|
||||
})
|
||||
}
|
||||
|
||||
override fun fileOpened(source: FileEditorManager, file: VirtualFile) {
|
||||
if (file is KiloVirtualFile) project.service<KiloVfsManager>().sync()
|
||||
}
|
||||
|
||||
override fun fileClosed(source: FileEditorManager, file: VirtualFile) {
|
||||
if (!closing && file is KiloVirtualFile) project.service<KiloVfsManager>().sync()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> Verify in `$INTELLIJ_REPO` that `com.intellij.openapi.project.ProjectCloseListener` with `TOPIC` + `projectClosing(project)` exists in the target platform. If not, use `com.intellij.openapi.project.ProjectManager.TOPIC` + `com.intellij.openapi.project.ProjectManagerListener.projectClosing`.
|
||||
|
||||
New `frontend/.../vfs/KiloVfsRestoreActivity.kt` — reopens the persisted set on startup (kinds are ensured by the provider, so no restore race):
|
||||
|
||||
```kotlin
|
||||
package ai.kilocode.client.vfs
|
||||
|
||||
import ai.kilocode.client.app.KiloWorkspaceService
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.startup.ProjectActivity
|
||||
|
||||
class KiloVfsRestoreActivity : ProjectActivity {
|
||||
override suspend fun execute(project: Project) {
|
||||
val base = project.basePath ?: return
|
||||
val ws = service<KiloWorkspaceService>()
|
||||
val dir = ws.resolveProjectDirectory(base)
|
||||
val paths = ws.virtualOpenPaths(dir)
|
||||
if (paths.isEmpty()) return
|
||||
ApplicationManager.getApplication().invokeLater({
|
||||
val mgr = project.service<KiloVfsManager>()
|
||||
paths.forEach { raw ->
|
||||
val parsed = KiloVirtualFileSystem.decode(raw) ?: return@forEach
|
||||
mgr.openLocal(parsed.kind, parsed.params, focus = false)
|
||||
}
|
||||
}, project.disposed)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Reopen uses `openLocal(kind, params)` (re-derives the path with the current `project.locationHash`), so a changed / split-mode hash is irrelevant. Reopening an already-open file is idempotent (`openFile` just focuses), so monolith platform-restore + this activity cannot double a tab.
|
||||
|
||||
### Step 13 — `frontend/src/main/resources/kilo.jetbrains.frontend.xml` (Phase 2 wiring)
|
||||
|
||||
Re-add, in place of the lines removed in Step 5 — the `postStartupActivity` inside `<extensions defaultExtensionNs="com.intellij">`:
|
||||
|
||||
```xml
|
||||
<postStartupActivity implementation="ai.kilocode.client.vfs.KiloVfsRestoreActivity"/>
|
||||
```
|
||||
|
||||
and a new top-level `<projectListeners>` block (sibling of `<extensions>` / `<actions>`):
|
||||
|
||||
```xml
|
||||
<projectListeners>
|
||||
<listener class="ai.kilocode.client.vfs.KiloVfsOpenTracker"
|
||||
topic="com.intellij.openapi.fileEditor.FileEditorManagerListener"/>
|
||||
</projectListeners>
|
||||
```
|
||||
|
||||
### Step 14 — Phase 2 tests
|
||||
|
||||
- `frontend/.../testing/FakeWorkspaceRpcApi.kt`: add fields + methods:
|
||||
```kotlin
|
||||
val openPathPushes = mutableListOf<Pair<String, List<String>>>()
|
||||
var openPaths = emptyList<String>()
|
||||
|
||||
override suspend fun setVirtualOpenPaths(directory: String, paths: List<String>) {
|
||||
assertNotEdt("setVirtualOpenPaths")
|
||||
openPathPushes.add(directory to paths)
|
||||
}
|
||||
|
||||
override suspend fun virtualOpenPaths(directory: String): List<String> {
|
||||
assertNotEdt("virtualOpenPaths")
|
||||
return openPaths
|
||||
}
|
||||
```
|
||||
- `frontend/.../app/KiloWorkspaceServiceTest.kt`: add a test that `virtualOpenPaths` returns the fake's list, and a test that `setVirtualOpenPaths` records a push (poll `rpc.openPathPushes` since the wrapper launches on the scope).
|
||||
- `frontend/.../vfs/KiloVfsManagerTest.kt` (keep `scope` / `rpc` / `waitFor`): add `testSyncPushesOpenKiloPaths` — open two kilo files via `openLocal`, call `project.service<KiloVfsManager>().sync()` on EDT, `waitFor { rpc.openPathPushes.isNotEmpty() }`, assert the last push's path list contains both canonical paths. (`<projectListeners>` from plugin.xml is not loaded in `BasePlatformTestCase`, so call `sync()` directly.)
|
||||
- `backend/.../vfs/KiloVfsOpenStoreTest.kt` (NEW, plain class — no fixture): `replace` then `paths` round-trips; a second `replace` overwrites; empty `replace` clears.
|
||||
- Optional: a restore test that, given `rpc.openPaths = [pathA, pathB]`, the reopen loop (the same `decode → openLocal` loop the activity uses) opens two `KiloVirtualFile`s with the current project hash.
|
||||
|
||||
### Phase 2 build gate (from `packages/kilo-jetbrains/`)
|
||||
|
||||
1. `./gradlew :frontend:test --tests "ai.kilocode.client.vfs.*" --tests "ai.kilocode.client.app.KiloWorkspaceServiceTest"`
|
||||
2. `./gradlew :backend:test --tests "ai.kilocode.backend.*"`
|
||||
3. `./gradlew typecheck`
|
||||
|
||||
---
|
||||
|
||||
# Final verification
|
||||
|
||||
- `./gradlew typecheck` and both `:test` sets all green.
|
||||
- Confirm no `kilocode_change` markers were added (these JetBrains paths are entirely Kilo-owned).
|
||||
- Manual split mode (`./gradlew runIdeBackend` + client, or the Split Mode run config): open attachment → exactly one real editor, **no blink, no JSON-named flash**; open 2 → **2** recents (not 4); reopen from Recent Files → real content; **restart with attachments open → they auto-reopen and re-fetch content**; previously-open attachment on startup → **no `Unknown Kilo editor kind` crash**.
|
||||
- Manual monolith (`./gradlew runIde`): same, plus confirm restart does not produce duplicate tabs.
|
||||
|
||||
# Changeset
|
||||
|
||||
Rewrite `.changeset/jetbrains-vfs-frontend-handoff.md` (keep the existing front-matter, e.g. `"kilo-code": patch`) to user-facing wording, e.g.: "Fix Kilo attachment editors in JetBrains: opening no longer flickers, Recent Files no longer duplicates entries, and open attachments reload after restarting the IDE."
|
||||
|
||||
# Pitfalls / notes
|
||||
|
||||
- **`createEditor` `error(...)`**: kept; unreachable for the only real kind (`attachment`, always ensured). Do not convert it to a silent fallback.
|
||||
- **Shutdown wipe**: the `closing` guard in `KiloVfsOpenTracker.fileClosed` is essential — without it, project teardown fires `fileClosed` for every tab and `sync()` would persist an empty set, breaking reopen. Verify the close topic (Step 12 note).
|
||||
- **Chatty `resolveProjectDirectory`**: `sync()` resolves the directory per call. Acceptable; if noisy, cache the resolved dir on `KiloVfsManager` after the first resolve.
|
||||
- **Keep backend VFS registered** (Step 6): removing it would make `kilo://` recents entries show raw JSON names or drop them.
|
||||
- Do **not** flip the `editor.rd.reopen.editors.on.frontend` registry key; our own restore activity handles split-mode reopen.
|
||||
|
||||
# Files summary
|
||||
|
||||
Phase 1 — edit: `frontend/.../vfs/KiloFileEditorProvider.kt`, `frontend/.../vfs/KiloVfsManager.kt`, `frontend/.../session/ui/attachment/AttachmentEditorKind.kt`, `frontend/src/main/resources/kilo.jetbrains.frontend.xml`, `backend/src/main/resources/kilo.jetbrains.backend.xml`, `backend/.../rpc/KiloWorkspaceRpcApiImpl.kt`, `shared/.../rpc/KiloWorkspaceRpcApi.kt`, `frontend/.../app/KiloWorkspaceService.kt`, `shared/.../vfs/KiloVirtualFile.kt`. Delete: `backend/.../vfs/KiloBackendFileEditorProvider.kt`, `frontend/.../vfs/KiloVfsReopenListener.kt`. Test edits: `FakeWorkspaceRpcApi.kt`, `KiloWorkspaceServiceTest.kt`, `KiloVfsManagerTest.kt`, `KiloVirtualFileSystemBackendTest.kt`.
|
||||
|
||||
Phase 2 — new: `backend/.../vfs/KiloVfsOpenStore.kt`, `frontend/.../vfs/KiloVfsOpenTracker.kt`, `frontend/.../vfs/KiloVfsRestoreActivity.kt`, `backend/.../vfs/KiloVfsOpenStoreTest.kt`. Edit: `shared/.../rpc/KiloWorkspaceRpcApi.kt`, `backend/.../rpc/KiloWorkspaceRpcApiImpl.kt`, `frontend/.../app/KiloWorkspaceService.kt`, `frontend/.../vfs/KiloVfsManager.kt`, `frontend/src/main/resources/kilo.jetbrains.frontend.xml`, `FakeWorkspaceRpcApi.kt`, `KiloWorkspaceServiceTest.kt`, `KiloVfsManagerTest.kt`. Plus the changeset rewrite.
|
||||
@@ -1,315 +0,0 @@
|
||||
# Fix JetBrains Kilo VFS Empty Split-Mode Editor
|
||||
|
||||
## Problem
|
||||
The latest split-mode fix made image-like Kilo virtual attachments open without the remote unsupported-file toast, but the opened tab is empty. The empty view is the backend placeholder editor from `KiloBackendFileEditorProvider` (`JPanel()`), not the frontend `KiloFileEditor` content.
|
||||
|
||||
After the first handoff implementation, manual testing still shows two tabs when clicking an attachment: one raw/encoded `kilo` backend wrapper tab with empty content, and one real `Kilo / ...` frontend tab with the image. If both are closed and the file is reopened from Recent Files, the empty backend wrapper can be reopened again.
|
||||
|
||||
## Root Cause
|
||||
- Backend-driven `OpenFileDescriptor(project, file).navigate(true)` needs a backend-side editor provider so the remote bridge considers `kilo` virtual files supported.
|
||||
- The backend support provider currently creates an actual empty `FileEditor`.
|
||||
- In split mode, that backend editor becomes the visible editor tab, so the frontend provider is not the final renderer for this open.
|
||||
- `EditorHistoryManager` only records useful entries from opened editor composites or fallback editor/provider pairs, so a backend open can satisfy history but must be followed by a deliberate frontend handoff.
|
||||
- `VirtualFile.getUrl()` is constructed as `protocol + "://" + path`. Our `KiloVirtualFileSystem.decode(...)` currently accepts only raw JSON paths, while real editor history/restoration and split-mode wrappers can surface `kilo://{...}` URL-shaped values. The current cleanup test only covered raw JSON wrapper paths, so real wrappers were not always identified.
|
||||
- Closing the backend wrapper tab is not enough. If its history entry remains, Recent Files can reopen the backend placeholder without going through `KiloVfsManager.open(...)`, so no handoff runs.
|
||||
|
||||
## Implementation Plan
|
||||
1. Keep the backend VFS and backend support provider, but treat it as temporary backend support/history plumbing only.
|
||||
- Keep it remote-dev-host gated.
|
||||
- Optionally change its component from a blank `JPanel` to a lightweight loading label so any transient state is diagnosable, but do not rely on it for final UI.
|
||||
- Do not add content bytes to `KiloVirtualFile`.
|
||||
|
||||
2. Change frontend `KiloVfsManager.open(path)` to perform a backend-to-frontend handoff.
|
||||
- Launch the existing coroutine.
|
||||
- Call `service<KiloWorkspaceService>().openVirtualPath(path)`.
|
||||
- If the backend call returns `true`, switch to EDT/Main and open the real frontend `KiloVirtualFile` with `openLocal(...)`.
|
||||
- This restores the real `AttachmentEditorKind.createContent(...)` image/text/binary UI while preserving the backend navigation step that avoids unsupported-file rejection and records backend history.
|
||||
|
||||
3. Normalize Kilo virtual paths before matching wrappers or accepting files.
|
||||
- Extend the shared path decode path to accept both raw JSON VFS paths and URL-shaped `kilo://{json}` values.
|
||||
- Do not change `KiloVirtualFileSystem.getPath(...)`; keep canonical raw JSON as the VFS path so existing `KiloVirtualFile` equality and history behavior remain stable.
|
||||
- Use the normalized decoder in `KiloVirtualFileSystem.findFileByPath(...)`, `KiloFileEditorProvider.path(...)`, and `KiloVfsManager` wrapper matching.
|
||||
- For wrapper matching, inspect both `file.path` and `file.url` when needed. Real split-mode wrappers may expose the `kilo` identity through the URL shape even if the wrapper object is not the exact frontend `KiloVirtualFile` instance.
|
||||
|
||||
4. Avoid duplicate empty tabs and stale Recent Files entries after the handoff.
|
||||
- After `openLocal(...)`, find backend wrapper files and history entries for the same canonical decoded `KiloPath`.
|
||||
- Identify wrappers by decodable canonical Kilo path and `file !is KiloVirtualFile`, not by raw string order.
|
||||
- Do not close the frontend `KiloVirtualFile` opened by `openLocal(...)`.
|
||||
- Prefer opening the frontend editor first, then closing only non-`KiloVirtualFile` wrappers with the same canonical path to avoid losing focus if the close happens before local open.
|
||||
- Remove matching non-`KiloVirtualFile` wrapper entries from `EditorHistoryManager` after closing them, while leaving the frontend `KiloVirtualFile` history entry in place. This ensures Recent Files reopens the real frontend editor, not the backend placeholder.
|
||||
|
||||
5. Add or adjust tests.
|
||||
- Update `KiloVfsManagerTest.testOpenUsesBackendVirtualFileRpc` so it expects the backend RPC call and then a frontend `KiloFileEditor` tab after the coroutine settles.
|
||||
- Add a focused test for handoff cleanup with a URL-shaped wrapper path (`kilo://{json}`): seed/open a decodable non-`KiloVirtualFile` wrapper, run the handoff cleanup, assert the wrapper is closed and removed from `EditorHistoryManager`, and assert the real `KiloVirtualFile`/`KiloFileEditor` remains.
|
||||
- Add a recents regression: after handoff cleanup, close the real frontend tab, reopen the remaining recent Kilo entry, and assert the selected editor is `KiloFileEditor` with real content rather than the backend placeholder.
|
||||
- Add shared VFS decode coverage for both raw JSON and `kilo://{json}` inputs.
|
||||
- Add provider coverage that a decodable URL-shaped `kilo` wrapper is accepted and creates real content.
|
||||
- Keep existing provider regression tests for decodable wrapped files and `HIDE_OTHER_EDITORS`.
|
||||
- Keep backend VFS decode/image classification tests.
|
||||
|
||||
6. Re-check attachment rendering.
|
||||
- Verify that after `open(...)`, selected editor is `KiloFileEditor` and its component is not the backend placeholder.
|
||||
- Verify only one visible tab remains for the attachment after the backend handoff settles.
|
||||
- Verify Recent Files reopens the `KiloVirtualFile` frontend editor, not the backend wrapper.
|
||||
- For test kind, assert the component text is still produced (`content:<id>`).
|
||||
- For attachment kind, existing tests should continue covering text/image/binary data loading paths if available; add a minimal UI assertion only if current tests do not exercise `AttachmentEditorKind.createContent(...)` after open.
|
||||
|
||||
## Verification
|
||||
Run from `packages/kilo-jetbrains/`:
|
||||
|
||||
1. `./gradlew :frontend:test --tests "ai.kilocode.client.vfs.KiloVfsManagerTest" --tests "ai.kilocode.client.vfs.KiloFileEditorProviderTest" --tests "ai.kilocode.client.vfs.KiloVirtualFileSystemTest" --tests "ai.kilocode.client.app.KiloWorkspaceServiceTest"`
|
||||
2. `./gradlew :backend:test --tests "ai.kilocode.backend.rpc.KiloVirtualFileSystemBackendTest"`
|
||||
3. `./gradlew typecheck`
|
||||
|
||||
## Expected Outcome
|
||||
Opening an image-like Kilo attachment in split mode should briefly pass through backend-supported navigation, then display the real frontend Kilo attachment editor with the image/content loaded. The unsupported-file toast should remain fixed, and the user should not be left on an empty backend placeholder tab.
|
||||
|
||||
---
|
||||
|
||||
# Follow-up: Recent Files reopen still shows an empty / JSON-named editor (split mode)
|
||||
|
||||
## New Symptom (reported)
|
||||
1. Clicking an attachment in the session opens the real editor correctly.
|
||||
2. The file then appears in Recent Files.
|
||||
3. Reopening it from Recent Files opens an **additional** tab with **empty content** and the **raw JSON path as the tab name** — not the real `KiloFileEditor`.
|
||||
4. Closing everything and reopening from Recent Files again yields an empty-content editor.
|
||||
|
||||
So the deliberate `KiloVfsManager.open()` handoff works, but the Recent Files / Switcher reopen path does not, because it never runs our handoff or cleanup.
|
||||
|
||||
## How Recent Files actually works here (from IntelliJ source)
|
||||
Verified against `$INTELLIJ_REPO` (`platform/recentFiles/**`, `platform/platform-impl/.../EditorHistoryManager.kt`):
|
||||
|
||||
- The Switcher reopens an entry by calling **frontend** `FileEditorManager.openFile(value.virtualFile)` (`recentFiles/frontend/switcherNavigation.kt:43`). There is a `com.intellij.recentFiles.navigator` EP that can change the open *mode* and a `com.intellij.recentFiles.excluder` EP (`RecentFilesExcluder`) that can hide entries — neither redirects which file is opened.
|
||||
- The Switcher model is built on the **backend** from `EditorHistoryManager.getInstance(project).fileList` plus the **frontend** editor selection history that the client passes in (`backendRecentFilesCollector.kt:36,102`; `frontendSwitcherItemsCollector.kt:29` → `FileEditorManagerImpl.getSelectionHistoryList()`).
|
||||
- `EditorHistoryManager.loadState` is a **no-op on JetBrains Client** (`EditorHistoryManager.kt:327`), so in split mode the frontend does not own persisted recents; the backend history + frontend live selection history drive the list.
|
||||
- A non-`KiloVirtualFile` wrapper is admitted to history when `VirtualFileManager.findFileByUrl(file.url) != null` (`EditorHistoryManager.kt:104-109`); a `kilo://{json}` url resolves via our VFS, so wrappers do get recorded.
|
||||
- The tab/switcher name comes from `presentableName` (`backendRecentFilesCollector.kt:186`). A raw-JSON name means the reopened file is **not** a `KiloVirtualFile` (whose `getName()` returns the filename) — it is a generic wrapper whose name falls back to its path, or the backend placeholder editor.
|
||||
|
||||
## Registration today (two providers, same editor type id)
|
||||
- Frontend `kilo.jetbrains.frontend.xml`: VFS `kilo` + `KiloFileEditorProvider` (EP id `KiloVfsEditor`, `getEditorTypeId() = "KiloVfsEditor"`).
|
||||
- Backend `kilo.jetbrains.backend.xml`: VFS `kilo` + `KiloBackendFileEditorProvider` (EP id `KiloVfsEditorBackend`, **but `getEditorTypeId()` is also `"KiloVfsEditor"`**). The backend editor is an empty `JPanel` (`KiloBackendFileEditorProvider.kt:43`), only active when `AppMode.isRemoteDevHost()` (or unit test).
|
||||
|
||||
## Root-cause hypotheses (need one log capture to disambiguate)
|
||||
The empty + JSON-named reopen means the file opened from recents is rendered by something other than the frontend `KiloFileEditor`. Two candidates:
|
||||
|
||||
- **H1 — backend-backed reopen.** The surviving recents entry is the backend `EditorHistoryManager` entry (the backend `navigate` recorded it and we never clean the backend side). Reopening routes the open to the backend, where `KiloBackendFileEditorProvider` produces the empty `JPanel`, projected to the client. Our frontend cleanup only touches the **frontend** `EditorHistoryManager`, so it cannot remove the backend entry.
|
||||
- **H2 — frontend wrapper reopen.** The recents entry is a client-side wrapper (from frontend selection history) that our deliberate-open `cleanup()` removed once, but a fresh wrapper is recorded again on the projected open and is never converted because reopen bypasses `KiloVfsManager.open()`. If its url/path is not in our decodable forms, `KiloFileEditorProvider.accept` returns false and a default empty editor is shown.
|
||||
|
||||
Both share the same fix shape: **a wrapper open from any source must be converted to the canonical frontend `KiloVirtualFile`, and the polluting entry must be removed on the side that owns it.**
|
||||
|
||||
## Confirmed context (from user)
|
||||
- **Environment: split mode / remote dev.** The backend `KiloBackendFileEditorProvider` (empty `JPanel`) is active, so H1 (backend-backed reopen) is the leading hypothesis. Both processes are involved.
|
||||
- **Approach: diagnostic logging first**, then implement the precise fix and remove the logging.
|
||||
|
||||
---
|
||||
|
||||
# EXECUTION PLAN (prescriptive — for a fast model)
|
||||
|
||||
## Rules for the implementer
|
||||
- Work only inside `packages/kilo-jetbrains/`. All target files are Kilo-owned (`ai/kilocode/...`); **do not add `kilocode_change` markers** (these paths are entirely Kilo additions).
|
||||
- Follow repo style: single-word names (`path`, `file`, `kind`, `cfg`), early returns, no `else`, no empty `catch` (use `log.error("...", err)` if you must catch). Prefer `const`/`val`. Do not reformat unrelated lines.
|
||||
- Java 21 is required for Gradle. Verify with `java -version` first; if missing, run `sdk install java 21-tem && sdk use java 21-tem`.
|
||||
- All editor/`FileEditorManager`/`EditorHistoryManager` calls run on EDT.
|
||||
- **Do Phase 1, then STOP and ask the user for logs.** Do not start Phase 2 until the user pastes the `[kilo-vfs]` log lines. The logs select which Phase 2 branch to implement.
|
||||
|
||||
---
|
||||
|
||||
## PHASE 1 — Diagnostic logging (do this first, then STOP)
|
||||
|
||||
Goal: capture, for both the working session-click open and the broken Recent Files reopen, exactly which side and which provider renders the file, and the file's concrete class / `path` / `url` / protocol / decodability. Every line below is prefixed `[kilo-vfs]` so it can be grepped.
|
||||
|
||||
### Edit 1 — `frontend/src/main/kotlin/ai/kilocode/client/vfs/KiloFileEditorProvider.kt`
|
||||
Add imports:
|
||||
```kotlin
|
||||
import com.intellij.idea.AppMode
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.util.PlatformUtils
|
||||
```
|
||||
In the `companion object`, add a logger and a log line inside `path(...)`:
|
||||
```kotlin
|
||||
companion object {
|
||||
const val EDITOR_TYPE_ID = "KiloVfsEditor"
|
||||
private val LOG = logger<KiloFileEditorProvider>()
|
||||
|
||||
private fun path(file: VirtualFile): KiloPath? {
|
||||
if (file is KiloVirtualFile) return file.path
|
||||
if (file.fileSystem.protocol != KiloVirtualFileSystem.PROTOCOL && !file.url.startsWith("${KiloVirtualFileSystem.PROTOCOL}://")) return null
|
||||
val path = KiloVirtualFileSystem.decode(file.path) ?: KiloVirtualFileSystem.decode(file.url)
|
||||
LOG.info("[kilo-vfs] front.path class=${file.javaClass.name} proto=${file.fileSystem.protocol} path=${file.path} url=${file.url} decoded=${path != null} client=${PlatformUtils.isJetBrainsClient()} host=${AppMode.isRemoteDevHost()}")
|
||||
if (path?.kind == "attachment") ensureAttachmentEditorKind()
|
||||
return path
|
||||
}
|
||||
}
|
||||
```
|
||||
In `createEditor(...)`, immediately before `return KiloFileEditor(...)`:
|
||||
```kotlin
|
||||
LOG.info("[kilo-vfs] front.createEditor kind=${kilo.path.kind} class=${file.javaClass.name} url=${file.url}")
|
||||
```
|
||||
|
||||
### Edit 2 — `frontend/src/main/kotlin/ai/kilocode/client/vfs/KiloVfsManager.kt`
|
||||
Add import `import com.intellij.openapi.diagnostic.logger`. Add a class-level field:
|
||||
```kotlin
|
||||
private val log = logger<KiloVfsManager>()
|
||||
```
|
||||
In `openLocal(path: String, focus: Boolean)`, after `val file = file(...) ?: return false`:
|
||||
```kotlin
|
||||
log.info("[kilo-vfs] front.openLocal handoff kind=${parsed.kind} params=${parsed.params}")
|
||||
```
|
||||
In `cleanup(...)`, log the counts (compute the matching lists once, log, then act):
|
||||
```kotlin
|
||||
val wrappers = manager.openFiles.filter { it.isWrapperFor(path) }
|
||||
val stale = history.fileList.filter { it.isWrapperFor(path) }
|
||||
log.info("[kilo-vfs] front.cleanup wrappersClosed=${wrappers.size} historyRemoved=${stale.size} path=${path.kind}/${path.params}")
|
||||
wrappers.forEach { manager.closeFile(it) }
|
||||
stale.forEach { history.removeFile(it) }
|
||||
```
|
||||
|
||||
### Edit 3 — `backend/src/main/kotlin/ai/kilocode/backend/vfs/KiloBackendFileEditorProvider.kt`
|
||||
Add imports:
|
||||
```kotlin
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.util.PlatformUtils
|
||||
```
|
||||
Add to the `companion object` a logger:
|
||||
```kotlin
|
||||
private val LOG = logger<KiloBackendFileEditorProvider>()
|
||||
```
|
||||
In `createEditor(...)`, before the `return`:
|
||||
```kotlin
|
||||
LOG.info("[kilo-vfs] back.createEditor class=${file.javaClass.name} path=${file.path} url=${file.url} host=${AppMode.isRemoteDevHost()} client=${PlatformUtils.isJetBrainsClient()}")
|
||||
```
|
||||
In `accept(...)`, just before the final `return KiloVirtualFileSystem.decode(file.path) != null`:
|
||||
```kotlin
|
||||
LOG.info("[kilo-vfs] back.accept class=${file.javaClass.name} proto=${file.fileSystem.protocol} path=${file.path} url=${file.url}")
|
||||
```
|
||||
|
||||
### Edit 4 — `backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceRpcApiImpl.kt`
|
||||
`LOG` already exists. In `openVirtualFile(path)`, after decoding/resolving:
|
||||
```kotlin
|
||||
LOG.info("[kilo-vfs] back.openVirtualFile path=$path decoded=${item} project=${project.locationHash}")
|
||||
```
|
||||
(place it after `val vf = ...findOrCreateFile(...)`, before `navigate(...)`).
|
||||
|
||||
### Build + hand off
|
||||
- Run `./gradlew typecheck` from `packages/kilo-jetbrains/`. It must pass.
|
||||
- **STOP. Tell the user:** run the split-mode repro (open an attachment, then reopen it from Recent Files), and paste every `[kilo-vfs]` line from BOTH logs — the JetBrains Client (frontend) log and the host (backend) log. In a Gradle split run these are the two run consoles / their respective `idea.log` sandbox files; grep for `[kilo-vfs]`.
|
||||
|
||||
### What the logs decide (Phase 2 branch selector)
|
||||
- **Branch A — frontend sees the reopen.** On reopen you see `front.path ... decoded=true`. The frontend provider is consulted, so a frontend listener can intercept it. Implement **Step 2A** (listener) + **Step 2C** (type id).
|
||||
- **Branch B — backend-only reopen.** On reopen you see `back.createEditor` (and/or `back.accept`) but NO `front.path`. The reopen never reaches the frontend, so a listener cannot help. Implement **Step 2B** (backend history exclusion) + **Step 2C** (type id), then re-test; add **Step 2A** as well if a transient wrapper tab still appears.
|
||||
- **Branch C — decode miss.** On reopen you see `front.path ... decoded=false`. The wrapper url/path shape is one our decoder does not recognize. Copy the exact `url`/`path` from the log into the plan and extend `KiloVirtualFileSystem.raw(...)` (Step 2D) to cover it; then Branch A applies.
|
||||
|
||||
---
|
||||
|
||||
## PHASE 2 — Fix (implement only the branch(es) the logs select)
|
||||
|
||||
### Step 2C — Always: give the backend provider a distinct editor type id
|
||||
The frontend and backend providers both return `getEditorTypeId() = "KiloVfsEditor"`. History serializes `selectedProvider` by type id, so a frontend entry can resolve to the backend empty editor on restore.
|
||||
|
||||
In `backend/.../vfs/KiloBackendFileEditorProvider.kt`, change:
|
||||
```kotlin
|
||||
const val EDITOR_TYPE_ID = "KiloVfsEditor"
|
||||
```
|
||||
to:
|
||||
```kotlin
|
||||
const val EDITOR_TYPE_ID = "KiloVfsEditorBackend"
|
||||
```
|
||||
(`getEditorTypeId()` already returns `EDITOR_TYPE_ID`; the frontend keeps `"KiloVfsEditor"`.)
|
||||
|
||||
### Step 2A — Frontend listener: adopt any wrapper open into the real editor
|
||||
New file `frontend/src/main/kotlin/ai/kilocode/client/vfs/KiloVfsReopenListener.kt`:
|
||||
```kotlin
|
||||
package ai.kilocode.client.vfs
|
||||
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
||||
import com.intellij.openapi.fileEditor.FileEditorManagerListener
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
|
||||
class KiloVfsReopenListener(private val project: Project) : FileEditorManagerListener {
|
||||
override fun fileOpened(source: FileEditorManager, file: VirtualFile) {
|
||||
if (file is KiloVirtualFile) return
|
||||
val path = KiloVirtualFileSystem.decode(file.path) ?: KiloVirtualFileSystem.decode(file.url) ?: return
|
||||
if (service<KiloVfsRegistry>().get(path.kind) == null) return
|
||||
ApplicationManager.getApplication().invokeLater({
|
||||
project.service<KiloVfsManager>().adopt(file, path)
|
||||
}, project.disposed)
|
||||
}
|
||||
}
|
||||
```
|
||||
Add `adopt(...)` to `KiloVfsManager` (reuses the proven `open()` handoff, which on success runs `openLocal` + `cleanup`, so it both renders the real editor and closes/removes the wrapper). Guard with a "real already open" check to converge and avoid repeated backend RPC:
|
||||
```kotlin
|
||||
@RequiresEdt
|
||||
fun adopt(wrapper: VirtualFile, path: KiloPath) {
|
||||
if (wrapper is KiloVirtualFile) return
|
||||
val canonical = path.copy(projectHash = project.locationHash)
|
||||
val manager = FileEditorManager.getInstance(project)
|
||||
val present = manager.openFiles.any { it is KiloVirtualFile && it.path == canonical }
|
||||
if (present) {
|
||||
manager.closeFile(wrapper)
|
||||
EditorHistoryManager.getInstance(project).removeFile(wrapper)
|
||||
return
|
||||
}
|
||||
open(KiloVirtualFileSystem.getInstance().getPath(canonical))
|
||||
}
|
||||
```
|
||||
Register the listener in `frontend/src/main/resources/kilo.jetbrains.frontend.xml` by adding a top-level `<projectListeners>` block (sibling of `<extensions>` and `<actions>`):
|
||||
```xml
|
||||
<projectListeners>
|
||||
<listener class="ai.kilocode.client.vfs.KiloVfsReopenListener"
|
||||
topic="com.intellij.openapi.fileEditor.FileEditorManagerListener"/>
|
||||
</projectListeners>
|
||||
```
|
||||
Convergence/reentry reasoning to verify in tests: `open()` → `openLocal` opens the canonical `KiloVirtualFile` → `fileOpened` fires for a `KiloVirtualFile` → listener returns immediately (`file is KiloVirtualFile`). Any re-projected wrapper hits the `present == true` branch and is just closed. No infinite loop.
|
||||
|
||||
### Step 2B — Backend: keep Kilo files out of the host's editor history (only for Branch B)
|
||||
In `shared/src/main/kotlin/ai/kilocode/client/vfs/KiloVirtualFile.kt`, add import `import com.intellij.idea.AppMode` and override so the file is recorded in client (frontend) recents but NOT in the backend host history (which is what drives the reopenable empty entry):
|
||||
```kotlin
|
||||
override fun isIncludedInEditorHistory(project: Project): Boolean = !AppMode.isRemoteDevHost()
|
||||
```
|
||||
(`KiloVirtualFile` already implements `EditorHistoryManager.IncludeInEditorHistoryFile`; this overrides its default `true`. `isPersistedInEditorHistory()` stays `false`.) Keep the backend support provider as-is — it remains the transient openability shim that prevents the unsupported-file toast; it must never be a durable recents target.
|
||||
|
||||
### Step 2D — Extend the decoder (only for Branch C)
|
||||
In `shared/.../vfs/KiloVirtualFileSystem.kt`, the private `raw(path)` currently accepts raw JSON, `kilo://{json}`, and `kilo://%7B...`. If the logs show a different wrapper shape (e.g. an rd-specific prefix wrapping the `kilo://` url), extend `raw(...)` to strip/recognize that exact shape and return the inner JSON. Add the observed example to a `KiloVirtualFileSystemTest` decode case. Do not broaden so far that unrelated non-JSON paths start decoding.
|
||||
|
||||
---
|
||||
|
||||
## PHASE 3 — Tests (extend existing suites; no mocks of EDT/threading)
|
||||
|
||||
All in `frontend/src/test/kotlin/ai/kilocode/client/vfs/`. Use the existing `KiloVfsManagerTest` helpers (`edt {}`, `waitFor {}`, `RemoteKiloFile`, `FakeWorkspaceRpcApi`).
|
||||
|
||||
1. **Adopt converts a reopened wrapper into the real editor (primary, Step 2A).** Open a decodable `RemoteKiloFile` wrapper directly via `FileEditorManager.openFile` (simulating a recents/projection reopen with no `KiloVfsManager.open`), then call `project.service<KiloVfsManager>().adopt(wrapper, canonical)` on EDT. `waitFor` until `rpc.virtualOpened` is non-empty, a `KiloVirtualFile` with the expected params is open, and the wrapper is gone from both `openFiles` and `EditorHistoryManager.fileList`. Assert selected editor is `KiloFileEditor` with `content:<id>` and exactly one `KiloVirtualFile` open.
|
||||
2. **Reentry guard.** `adopt(realKiloVirtualFile, path)` is a no-op: no extra RPC, no close. Open the real file first, snapshot `rpc.virtualOpened.size`, call `adopt` with it, assert size unchanged and the editor still open.
|
||||
3. **Present-guard (no duplicate RPC).** With the canonical `KiloVirtualFile` already open, calling `adopt(wrapper, canonical)` closes the wrapper and removes its history entry without adding to `rpc.virtualOpened`.
|
||||
4. **Optional listener wiring.** Publish to the topic in-process and assert the same outcome as test 1:
|
||||
`project.messageBus.syncPublisher(FileEditorManagerListener.FILE_EDITOR_MANAGER).fileOpened(manager, wrapper)` then drain EDT via `UIUtil.dispatchAllInvocationEvents()`.
|
||||
5. **Type id (Step 2C).** Assert `KiloBackendFileEditorProvider().editorTypeId == "KiloVfsEditorBackend"` (add/keep this in the backend test module if a backend unit test exists; otherwise assert the frontend stays `"KiloVfsEditor"` in `KiloFileEditorProviderTest`).
|
||||
6. Keep all existing `KiloVfsManagerTest` / `KiloFileEditorProviderTest` / `KiloVirtualFileSystemTest` / `AttachmentEditorKindTest` cases green.
|
||||
|
||||
---
|
||||
|
||||
## PHASE 4 — Cleanup
|
||||
- Remove every Phase 1 `[kilo-vfs]` `log.info` line and any now-unused imports (`logger`, `PlatformUtils`, `AppMode`) that were added only for logging. Keep imports/loggers only where Phase 2 still uses them.
|
||||
- Re-run typecheck.
|
||||
|
||||
---
|
||||
|
||||
## Verification (run from `packages/kilo-jetbrains/`)
|
||||
1. `./gradlew :frontend:test --tests "ai.kilocode.client.vfs.*" --tests "ai.kilocode.client.session.ui.attachment.AttachmentEditorKindTest"`
|
||||
2. `./gradlew :backend:test --tests "ai.kilocode.backend.rpc.KiloVirtualFileSystemBackendTest"`
|
||||
3. `./gradlew typecheck`
|
||||
4. Manual split mode: open attachment → exactly one real `KiloFileEditor`; reopen from Recent Files → one real `KiloFileEditor`, no empty/JSON-named duplicate; close all and reopen again → real content.
|
||||
|
||||
## Expected outcome
|
||||
Reopening a Kilo attachment from Recent Files in split mode always lands on the real frontend `KiloFileEditor` with content, with no empty/JSON-named duplicate tab and no duplicate recents entry, while the unsupported-file toast stays fixed.
|
||||
|
||||
## Resolved decisions
|
||||
1. **Repro environment:** split mode / remote dev (backend provider active). — confirmed.
|
||||
2. **Diagnostic logging first:** land Phase 1 logging, capture one recents reopen, then implement the targeted Phase 2 branch and remove the logging. — confirmed.
|
||||
|
||||
## Files touched (summary)
|
||||
- Phase 1 (temporary): `frontend/.../vfs/KiloFileEditorProvider.kt`, `frontend/.../vfs/KiloVfsManager.kt`, `backend/.../vfs/KiloBackendFileEditorProvider.kt`, `backend/.../rpc/KiloWorkspaceRpcApiImpl.kt`.
|
||||
- Phase 2: `backend/.../vfs/KiloBackendFileEditorProvider.kt` (type id), `frontend/.../vfs/KiloVfsReopenListener.kt` (new), `frontend/.../vfs/KiloVfsManager.kt` (`adopt`), `frontend/src/main/resources/kilo.jetbrains.frontend.xml` (listener); conditionally `shared/.../vfs/KiloVirtualFile.kt` (Branch B) and `shared/.../vfs/KiloVirtualFileSystem.kt` (Branch C).
|
||||
- Phase 3: `frontend/src/test/kotlin/ai/kilocode/client/vfs/KiloVfsManagerTest.kt` (+ provider/decoder tests as needed).
|
||||
@@ -1,65 +0,0 @@
|
||||
# JetBrains Kilo VFS Frontend Recent Files Plan
|
||||
|
||||
## Goal
|
||||
|
||||
Make Kilo JetBrains attachment editor files show as distinct Recent Files entries while open and remain in Recent Files after their tabs are closed, including in the current split/native Recent Files implementation.
|
||||
|
||||
## Findings
|
||||
|
||||
- The previous stable-key/cache fix is still useful, but it only proves `EditorHistoryManager.fileList` can retain stable `KiloVirtualFile` instances.
|
||||
- The user-visible `RecentFiles` action is the new frontend Recent Files implementation, not only the older `EditorHistoryManager`/fallback switcher path.
|
||||
- IntelliJ's new Recent Files flow renders `FrontendRecentFilesModel`, populated by `RecentlySelectedEditorListener` plus backend metadata/history updates.
|
||||
- Kilo attachment files are frontend-created non-physical virtual files. In split/native Recent Files, backend history is not a reliable source for frontend-only Kilo files after close.
|
||||
- `RECENTLY_OPENED_UNPINNED` intentionally removes files after close. When it becomes empty or size one, the frontend model falls back to `RECENTLY_OPENED`; Kilo closed attachments must therefore be explicitly present in `RECENTLY_OPENED`.
|
||||
- The platform exposes `FrontendRecentFilesModel.applyFrontendChanges(...)` and `RecentFileKind`/`FileChangeKind` as internal APIs. We already rely on internal/experimental editor-history APIs, so using this narrowly for Kilo VFS files is acceptable if isolated and tested.
|
||||
- Two attachments with the same filename can still look identical because `AttachmentEditorKind.title(...)` and `presentablePath(...)` currently use only `sessionId` and filename. Distinct model entries should be asserted by `KiloPath`, and display disambiguation should be considered if tests show visually identical rows.
|
||||
|
||||
## Approach
|
||||
|
||||
Add a Kilo-owned frontend recents bridge for `KiloVirtualFile` instances. It will keep a small per-project MRU list of valid Kilo VFS files and explicitly feed those files into `FrontendRecentFilesModel.RECENTLY_OPENED` on open and after close. This supplements platform history without changing editor-history persistence or moving Kilo VFS files to backend/shared modules.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Add frontend Recent Files regression coverage.
|
||||
- In `KiloVfsManagerTest`, open two distinct `KiloVfsTestKind` files and wait for `FrontendRecentFilesModel.getInstance(project).getRecentFiles(RecentFileKind.RECENTLY_OPENED)` to contain both corresponding `KiloVirtualFile`s.
|
||||
- Close both files and assert `FileEditorManager.openFiles` has none of them while `RECENTLY_OPENED` still contains both.
|
||||
- Keep the existing `EditorHistoryManager.fileList` assertions as fallback-switcher coverage.
|
||||
|
||||
2. Add attachment-specific frontend Recent Files coverage.
|
||||
- In `AttachmentEditorKindTest`, open two embedded attachments with the same `partId`/filename and different URLs, using `attachmentParams(...)` so `attachmentKey` differs.
|
||||
- Assert `RECENTLY_OPENED` contains two attachment `KiloVirtualFile`s with both `attachmentKey` values while open.
|
||||
- Close both tabs and assert `RECENTLY_OPENED` still contains the same two attachment keys.
|
||||
- Assert `RECENTLY_OPENED_UNPINNED` does not need to retain closed files; the important closed-file surface is `RECENTLY_OPENED`.
|
||||
|
||||
3. Add a small Kilo-owned bridge service.
|
||||
- Create a project-level light service, for example `KiloVfsRecentFiles` under `ai.kilocode.client.vfs`.
|
||||
- Store recent Kilo files by canonical `KiloPath`, newest first, using the cached `KiloVirtualFile` instances.
|
||||
- Filter invalid files and cap the list to `UISettings.getInstance().recentFilesLimit + 1` or a small safe bound if `UISettings` is awkward in tests.
|
||||
- Isolate all imports of `com.intellij.platform.recentFiles.frontend.model.FrontendRecentFilesModel`, `RecentFileKind`, and `FileChangeKind` in this service.
|
||||
|
||||
4. Feed the frontend Recent Files model from `KiloVfsManager`.
|
||||
- After `FileEditorManager.openFile(file, focus)`, call the bridge to record the file and apply it to `RECENTLY_OPENED` and `RECENTLY_OPENED_UNPINNED` with `FileChangeKind.ADDED`.
|
||||
- After `FileEditorManager.closeFile(file)`, call the bridge to re-apply the cached Kilo MRU list to `RECENTLY_OPENED` only, so closed Kilo files remain recent but do not pollute the unpinned/open-editor switcher state.
|
||||
- Keep `isPersistedInEditorHistory() = false` unchanged.
|
||||
|
||||
5. Preserve distinct identity and optional display disambiguation.
|
||||
- Keep identity based on canonical `KiloPath` including `attachmentKey`.
|
||||
- If the frontend model tests show two entries exist but are visually indistinguishable, update `AttachmentEditorKind.presentablePath(...)` to include a stable discriminator such as `messageId` and a short `attachmentKey` suffix while keeping the tab title as the filename.
|
||||
- Add a presentation test only if this display change is needed.
|
||||
|
||||
6. Verify.
|
||||
- From `packages/kilo-jetbrains/`, run:
|
||||
- `./gradlew :frontend:test --tests ai.kilocode.client.vfs.KiloVfsManagerTest --tests ai.kilocode.client.vfs.KiloVirtualFileSystemTest --tests ai.kilocode.client.session.ui.attachment.AttachmentEditorKindTest`
|
||||
- `./gradlew typecheck`
|
||||
|
||||
## Expected Result
|
||||
|
||||
- Two distinct attachments appear as two Recent Files model entries while open.
|
||||
- Closing all attachment tabs leaves both attachments in `RECENTLY_OPENED` during the current IDE session.
|
||||
- Closed attachments are not persisted across IDE restarts.
|
||||
- Reopening the exact same attachment still reuses the same editor tab because `KiloPath`/`KiloVirtualFile` identity remains unchanged.
|
||||
|
||||
## Caveats
|
||||
|
||||
- This uses IntelliJ's internal frontend Recent Files model API in a narrow Kilo VFS boundary. The alternative would be a broader split-mode redesign that makes Kilo VFS files backend-originated, which is much larger and unnecessary for this bug.
|
||||
- Search Everywhere's `RecentFilesSEContributor` is a different surface and maps files through `PsiManager.findFile(...)`; this plan targets the `RecentFiles` action described by the user.
|
||||
@@ -1,54 +0,0 @@
|
||||
# Fix JetBrains Kilo VFS Image-Like Backend Open
|
||||
|
||||
## Problem
|
||||
Opening a Kilo virtual attachment whose displayed filename ends in an image extension, such as `.png`, must go through the backend/split open path so it appears in the IDE's backend-owned recent/editor history behavior. However, backend opening currently lets IntelliJ image infrastructure classify the contentless Kilo virtual file as an image. `ImageFileService` calls `IfsUtil.getImageProvider(file)`, which calls `file.contentsToByteArray()`. `KiloVirtualFile` intentionally throws from content accessors because these are contentless UI tabs, so the backend logs `UnsupportedOperationException`.
|
||||
|
||||
## Updated Learning
|
||||
- The previous implementation that moved `KiloVfsManager.open(...)` to frontend-local `FileEditorManager.openFile(...)` was wrong for product behavior: Kilo virtual attachments need to be opened from the backend path so they participate in the expected recents/history behavior.
|
||||
- The backend virtual-open RPC path is intentional and should remain:
|
||||
- `KiloVfsManager.open(...)` should call `KiloWorkspaceService.openVirtualPath(...)` asynchronously.
|
||||
- `KiloWorkspaceService.openVirtualPath(...)` should call `KiloWorkspaceRpcApi.openVirtualFile(...)`.
|
||||
- `KiloWorkspaceRpcApiImpl.openVirtualFile(...)` should decode the Kilo virtual path, resolve the backend project, create the backend-side `KiloVirtualFile`, and navigate it with `OpenFileDescriptor`.
|
||||
- The image crash root cause is still file-type assignment, not backend opening itself:
|
||||
- `KiloVirtualFile` extends `LightVirtualFileBase("", null, 0)` in the old/broken state.
|
||||
- `LightVirtualFileBase` already implements `VirtualFileWithAssignedFileType`; passing `null` leaves the assigned file type unset.
|
||||
- IntelliJ `FileTypeRegistry.isFileOfType(file, ImageFileType.INSTANCE)` checks assigned file type first, then falls back to filename/extension detection when assigned type is `null`.
|
||||
- A displayed filename like `screen.png` is enough to trigger image classification even though `KiloVirtualFile.getFileType()` returns `FileTypes.UNKNOWN`.
|
||||
- The correct focused fix is to pass `FileTypes.UNKNOWN` into the `LightVirtualFileBase` constructor while keeping content accessors throwing.
|
||||
|
||||
## Implementation Plan
|
||||
1. Revert the latest frontend-local opening changes, but do not lose the file-type fix:
|
||||
- Restore `KiloVfsManager` to accept the service `CoroutineScope` constructor dependency.
|
||||
- Restore `KiloVfsManager.open(kind, params)` / `open(path)` so they launch a coroutine and call `service<KiloWorkspaceService>().openVirtualPath(path)`.
|
||||
- Keep `openLocal(...)` only as the local/test helper path that already existed before the incorrect frontend-local `open(...)` change.
|
||||
2. Restore the backend virtual-open RPC contract:
|
||||
- Re-add `KiloWorkspaceService.openVirtualPath(path)` with the previous try/catch logging wrapper around `call { openVirtualFile(path) }`.
|
||||
- Re-add `KiloWorkspaceRpcApi.openVirtualFile(path)`.
|
||||
- Re-add `KiloWorkspaceRpcApiImpl.openVirtualFile(path)` and the `project(path: KiloPath)` helper that resolves by project hash, `directory` param, then first open project.
|
||||
- Re-add the needed `KiloPath` and `KiloVirtualFileSystem` imports in the backend implementation.
|
||||
- Re-add fake RPC tracking for `virtualOpened` and the fake `openVirtualFile(...)` implementation.
|
||||
3. Keep the actual image-classification fix:
|
||||
- Ensure `packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/client/vfs/KiloVirtualFile.kt` uses `LightVirtualFileBase("", FileTypes.UNKNOWN, 0)`.
|
||||
- Do not change `contentsToByteArray()`, `getInputStream()`, or `getOutputStream()` to return dummy content.
|
||||
- Keep `VirtualFileWithoutContent`.
|
||||
4. Update tests back to backend-open semantics:
|
||||
- Restore the `KiloVfsManagerTest` setup that replaces `KiloWorkspaceService` with `FakeWorkspaceRpcApi`.
|
||||
- Restore the `open(...)` test so it asserts a canonical generic virtual path is sent to `rpc.virtualOpened`, not that a frontend editor tab opens directly.
|
||||
- Keep `openLocal(...)` tests for direct frontend editor behavior under their local helper path.
|
||||
- Restore the `KiloWorkspaceServiceTest` case asserting `openVirtualPath("virtual-path")` calls backend RPC directly.
|
||||
5. Keep/add regression tests for the actual image bug:
|
||||
- Keep the existing `KiloFileEditorProvider` `HIDE_OTHER_EDITORS` image-like-name test because it still guards frontend provider selection.
|
||||
- Keep or add a `KiloVirtualFileSystemTest`/VFS regression for an image-like filename such as `screen.png` asserting `FileTypeRegistry.getInstance().getFileTypeByFile(file)` is `FileTypes.UNKNOWN` and `FileTypeRegistry.getInstance().isFileOfType(file, FileTypes.UNKNOWN)` is true.
|
||||
- If practical, add the same assertion in a backend-side VFS/RPC-adjacent test to document that backend-created Kilo virtual files also block image classification.
|
||||
6. Preserve unrelated dirty worktree changes:
|
||||
- Do not use `git checkout`, `git reset`, or broad reverts.
|
||||
- Apply a targeted patch that only undoes the incorrect frontend-local/RPC-removal edits and retains the `FileTypes.UNKNOWN` constructor assignment.
|
||||
|
||||
## Verification
|
||||
1. From `packages/kilo-jetbrains/`, confirm Java 21 with `java -version`.
|
||||
2. Run targeted tests: `./gradlew :frontend:test --tests "ai.kilocode.client.vfs.KiloVfsManagerTest" --tests "ai.kilocode.client.vfs.KiloFileEditorProviderTest" --tests "ai.kilocode.client.vfs.KiloVirtualFileSystemTest" --tests "ai.kilocode.client.app.KiloWorkspaceServiceTest"`.
|
||||
3. If a backend VFS regression test is added, run it explicitly, for example `./gradlew :backend:test --tests "ai.kilocode.backend.rpc.KiloVirtualFileSystemBackendTest"`.
|
||||
4. Run package typecheck: `./gradlew typecheck`.
|
||||
|
||||
## Expected Outcome
|
||||
Kilo virtual attachments are opened through the backend path again, preserving recents/history behavior. Image-like display names no longer classify contentless Kilo virtual files as image files, so backend image infrastructure does not call unsupported content accessors. The Kilo editor provider still hides competing frontend editors for Kilo virtual files.
|
||||
@@ -1,72 +0,0 @@
|
||||
# JetBrains Kilo VFS Recent Files Retention Plan
|
||||
|
||||
## Goal
|
||||
|
||||
Fix Kilo JetBrains attachment editor tabs so every distinct attachment remains visible as a distinct Recent Files entry after its editor tab is closed, while reopening the exact same attachment still reuses the existing tab.
|
||||
|
||||
## Current Findings
|
||||
|
||||
- `Recent Files` is not only a display problem. IntelliJ builds the switcher/recent-file surfaces from `EditorHistoryManager.fileList` plus currently open editor files, then deduplicates by `VirtualFile.equals/hashCode`.
|
||||
- `RecentFilesSEContributor` additionally filters out currently-open files and only includes closed history files when `vf.isValid()` is true.
|
||||
- The previous `attachmentKey` fix addresses one identity bug: duplicate or blank `partId` values now produce distinct `KiloPath` params. It does not cover post-close retention.
|
||||
- Existing tests assert history immediately after open, but no test asserts that Kilo VFS files remain in `EditorHistoryManager.fileList` after closing their editor tabs.
|
||||
- `KiloVirtualFileSystem.findOrCreateFile(...)` currently creates a fresh `KiloVirtualFile` for each lookup. IntelliJ's identity virtual-file pointer path is URL-keyed and captures a file instance, so transient instances are the likely cause of unreliable post-close recents behavior.
|
||||
|
||||
## Approach
|
||||
|
||||
Introduce a stable per-project Kilo VFS file cache keyed by canonical `KiloPath`, then add regression coverage for post-close history retention.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Add a failing generic post-close regression test in `KiloVfsManagerTest`.
|
||||
- Open two distinct `KiloVfsTestKind` files.
|
||||
- Flush EDT events.
|
||||
- Close both via `KiloVfsManager.close(...)` or `FileEditorManager.closeFile(...)`.
|
||||
- Assert there are no open Kilo test files.
|
||||
- Assert `EditorHistoryManager.getInstance(project).fileList.filterIsInstance<KiloVirtualFile>()` still contains both canonical paths and both files are valid.
|
||||
|
||||
2. Add an attachment-specific post-close regression test in `AttachmentEditorKindTest`.
|
||||
- Build two attachments with same `partId`/filename/mime and different `data:` URLs, using `attachmentParams(...)`.
|
||||
- Open both and flush.
|
||||
- Close both and flush.
|
||||
- Assert no attachment Kilo files are open.
|
||||
- Assert history still contains two attachment entries with different `attachmentKey` values.
|
||||
- This test covers the exact user symptom: distinct attachments should still appear in recents after closing tabs.
|
||||
|
||||
3. Add VFS instance-stability coverage in `KiloVirtualFileSystemTest`.
|
||||
- Call `findFileByPath(...)` twice for the same serialized path and assert `assertSame(...)`.
|
||||
- Call with equivalent params in different insertion order and assert the same cached file is returned.
|
||||
- Call with distinct params and assert a different file is returned.
|
||||
|
||||
4. Implement a per-project Kilo VFS file cache.
|
||||
- Prefer a small project-level light service, e.g. `@Service(Service.Level.PROJECT) class KiloVfsFileCache(private val project: Project)`.
|
||||
- Store `ConcurrentHashMap<KiloPath, KiloVirtualFile>` keyed by `path.canonical()`.
|
||||
- Expose a method that returns an existing valid cached file for the canonical path or creates/stores a new `KiloVirtualFile(project, canonical)`.
|
||||
- Keep the cache project-scoped so project disposal cleans up references and avoids leaking `Project` from an application-level VFS singleton.
|
||||
|
||||
5. Route all VFS file creation through the cache.
|
||||
- In `KiloVirtualFileSystem.findOrCreateFile(project, path)`, canonicalize the path and verify the kind is registered as today.
|
||||
- Return `project.service<KiloVfsFileCache>().findOrCreate(canonical)` instead of constructing `KiloVirtualFile` directly.
|
||||
- Preserve `getPath(...)` and `decode(...)` canonicalization from the stable-key work.
|
||||
|
||||
6. Keep identity semantics unchanged unless tests prove otherwise.
|
||||
- Leave `KiloVirtualFile.equals/hashCode` as `project + path`, so existing open-file dedup and tab reuse behavior remains stable.
|
||||
- Do not enable editor history persistence to disk; keep `isPersistedInEditorHistory() = false` as required.
|
||||
|
||||
7. Verify the fix.
|
||||
- From `packages/kilo-jetbrains/`, run targeted frontend tests:
|
||||
- `./gradlew :frontend:test --tests ai.kilocode.client.vfs.KiloVfsManagerTest --tests ai.kilocode.client.vfs.KiloVirtualFileSystemTest --tests ai.kilocode.client.session.ui.attachment.AttachmentEditorKindTest`
|
||||
- Run `./gradlew typecheck` from `packages/kilo-jetbrains/`.
|
||||
- If the Gradle selector applies unexpectedly to backend tasks, rerun the same filters against `:frontend:test` only.
|
||||
|
||||
## Expected Result
|
||||
|
||||
- Opening the same attachment twice still reuses one tab/history entry.
|
||||
- Opening distinct attachments, including duplicate `partId` attachments, creates distinct tabs/history entries.
|
||||
- Closing attachment tabs no longer removes them from in-memory Recent Files during the IDE session.
|
||||
- Recent-file history remains non-persistent across IDE restarts, preserving the existing product decision.
|
||||
|
||||
## Notes
|
||||
|
||||
- This fix should stay entirely under `packages/kilo-jetbrains/`; no shared upstream opencode files are involved.
|
||||
- The IntelliJ APIs involved (`EditorHistoryManager.IncludeInEditorHistoryFile`) are already in use and marked internal/experimental upstream; this plan does not introduce a new dependency category.
|
||||
@@ -1,30 +0,0 @@
|
||||
# JetBrains Kilo VFS Recent Files Plan
|
||||
|
||||
## Goal
|
||||
Ensure every distinct Kilo VFS editor tab opened by the JetBrains plugin appears as a distinct item in IntelliJ Recent Files, while reopening the exact same Kilo VFS file still reuses the existing editor tab.
|
||||
|
||||
## Findings
|
||||
- The PR adds the Kilo VFS/editor stack in `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/vfs/` and opens embedded attachments through `SessionUi.openAttachment()`.
|
||||
- `KiloVirtualFile` already opts into `EditorHistoryManager.IncludeInEditorHistoryFile` and `KiloVfsManager.open()` uses `FileEditorManager.openFile(...)`, so the integration point is correct.
|
||||
- IntelliJ recent-file source in `$INTELLIJ_REPO` shows Recent Files is built from `EditorHistoryManager.fileList` plus `FileEditorManager.openFiles`, then deduplicated with `HashSet`/`subtract`. Distinct Kilo files therefore need distinct `VirtualFile.equals/hashCode` identity.
|
||||
- `KiloVirtualFile.equals/hashCode` uses `project + KiloPath`; `KiloPath` includes `params`. Any Kilo editor kind that builds identical params for different user-visible files will collapse into one recent-file entry.
|
||||
- `AttachmentEditorKind.attachmentParams(...)` currently includes `sessionId`, `messageId`, `partId`, `filename`, `mime`, and `directory`, but not the attachment URL/content identity. Duplicate or blank `partId` values can make different embedded attachments share the same Kilo path.
|
||||
|
||||
## Implementation
|
||||
1. Add a failing regression test before changing behavior.
|
||||
2. In `AttachmentEditorKindTest`, create two embedded `FileAttachment` values with the same `id`, same filename, same mime, same session/message/directory, and different `data:` URLs. Assert `attachmentParams(...)` differs and that opening both params creates two open `KiloVirtualFile` tabs and two `EditorHistoryManager.fileList` entries.
|
||||
3. In `KiloVfsManagerTest`, add or strengthen a generic expectation that two distinct Kilo VFS paths remain two distinct open files and two distinct editor-history entries. This documents the Recent Files contract for all Kilo editor kinds.
|
||||
4. Update `attachmentParams(...)` to add a compact stable identity field, such as `attachmentKey`, derived from the attachment fields that distinguish real files. Use a hash rather than storing the full `data:` URL in the VFS path.
|
||||
5. Keep the exact same params for the exact same attachment deterministic, so reopening the same attachment still focuses/reuses the same editor instead of duplicating tabs.
|
||||
6. Update `KiloAttachmentEditorService.fetch(...)` to use the new identity field when resolving file parts, so duplicate or blank `partId` values do not load the wrong attachment content.
|
||||
7. Update attachment presentation/validity tests for the new param key. Keep existing required params unless the new key is required by the production open path.
|
||||
8. Add a patch changeset for `kilo-jetbrains`, because this is a user-visible JetBrains plugin bug fix.
|
||||
|
||||
## Verification
|
||||
- Run `./gradlew test --tests ai.kilocode.client.vfs.KiloVfsManagerTest --tests ai.kilocode.client.session.ui.attachment.AttachmentEditorKindTest` from `packages/kilo-jetbrains/`.
|
||||
- Run `./gradlew typecheck` from `packages/kilo-jetbrains/`.
|
||||
- If the targeted Gradle test selector is not accepted by this build, run the nearest frontend test task or `./gradlew test` from `packages/kilo-jetbrains/`.
|
||||
|
||||
## Notes
|
||||
- No IntelliJ source changes are needed.
|
||||
- `EditorHistoryManager.IncludeInEditorHistoryFile` is marked internal/experimental in IntelliJ source, but the PR already uses it; this plan does not add a new IntelliJ dependency beyond validating current behavior.
|
||||
@@ -1,48 +0,0 @@
|
||||
# JetBrains Kilo VFS Stable Keys Plan
|
||||
|
||||
## Goal
|
||||
Make Kilo VFS editor files use stable, semantic identity so every distinct attachment can appear as a distinct Recent Files item, reopening the same logical VFS file reuses the same editor tab, and future VFS content kinds can be added without process-launch or random identifiers in their file keys.
|
||||
|
||||
## Findings
|
||||
- The JetBrains plugin VFS is in `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/vfs/`.
|
||||
- `KiloVfsManager` currently creates `launchId = System.currentTimeMillis().toString()` and passes it into `KiloPath` for every VFS file.
|
||||
- `KiloPath` serializes `launchId`, `projectHash`, `kind`, and `params`; `KiloVirtualFile.equals/hashCode` includes the full `KiloPath`, so launch-scoped data is part of file identity.
|
||||
- `KiloVirtualFile` is included in editor history but `isPersistedInEditorHistory()` returns `false`; keep persistence disabled for now and do not use volatile path keys as a persistence workaround.
|
||||
- The attachment-specific discriminator `attachmentKey` already exists in this worktree and distinguishes duplicate file parts with the same `partId`; keep that deterministic discriminator, but remove the generic launch-scoped part of the VFS key.
|
||||
|
||||
## Target Design
|
||||
- Treat a Kilo VFS file path as `projectHash + kind + stable params` only.
|
||||
- Remove `launchId`, timestamps, random UUIDs, editor instance IDs, and process-local values from `KiloPath` and from all VFS path-building helpers.
|
||||
- Canonicalize params before serializing a VFS path so two maps with the same key/value set produce the same path string regardless of insertion order.
|
||||
- Keep each editor kind responsible for building stable params through a small kind-owned helper, rather than constructing ad hoc maps at call sites.
|
||||
- For attachments, use stable params such as `sessionId`, `messageId`, `partId`, `attachmentKey`, and `directory`; keep `filename` and `mime` only as stable presentation/fetch hints.
|
||||
- For future kinds, define one helper per kind:
|
||||
- Session UI: `sessionId` and `directory`.
|
||||
- Marketplace page: stable route or listing id, plus any stable item id needed to identify the page.
|
||||
- Generated/session artifacts: session id plus message/part/content id, with a deterministic content discriminator only when the backend lacks a unique part id.
|
||||
|
||||
## Implementation Steps
|
||||
1. Update `KiloPath` to remove `launchId`; fields become `projectHash`, `kind`, and `params`.
|
||||
2. Update `KiloVfsManager` to stop storing `launchId` and create paths from `project.locationHash`, `kind`, and canonicalized params.
|
||||
3. Add a small canonicalization helper near the VFS core, for example sorting params by key before `KiloVirtualFileSystem.getPath(...)` serializes them and before decoded paths become `KiloVirtualFile` instances.
|
||||
4. Update tests and test helpers that construct `KiloPath("launch", ...)` to the new stable constructor/signature.
|
||||
5. Strengthen `KiloVfsManagerTest` or `KiloVirtualFileSystemTest` with assertions that:
|
||||
- serialized Kilo paths do not contain `launchId` or any launch value,
|
||||
- the same kind and same params produce the same serialized path across independent `KiloPath` constructions,
|
||||
- param insertion order does not change serialized path identity,
|
||||
- opening the same stable key twice reuses one editor tab,
|
||||
- opening two distinct stable keys creates two open files and two Recent Files entries.
|
||||
6. Keep the existing attachment tests that assert duplicate `partId` attachments get distinct `attachmentKey` values and distinct history entries.
|
||||
7. Update `AttachmentEditorKindTest` direct `KiloPath` construction to the new stable path shape and add an explicit assertion that attachment params do not include launch/time/random keys.
|
||||
8. Keep `KiloVirtualFile.isPersistedInEditorHistory()` returning `false`; do not add persistence behavior in this fix.
|
||||
9. Update the existing JetBrains changeset description if needed so release notes describe stable/distinct Kilo VFS attachment tabs from the user perspective.
|
||||
|
||||
## Non-Goals
|
||||
- Do not persist Kilo VFS editor tabs yet.
|
||||
- Do not migrate old launch-scoped paths unless a concrete persisted-state need appears; stale old paths can fail to decode or be ignored because VFS history persistence is currently disabled.
|
||||
- Do not add session UI or marketplace VFS kinds in this fix; only make the VFS identity model ready for them.
|
||||
|
||||
## Verification
|
||||
- From `packages/kilo-jetbrains/`, run `./gradlew test --tests ai.kilocode.client.vfs.KiloVfsManagerTest --tests ai.kilocode.client.vfs.KiloVirtualFileSystemTest --tests ai.kilocode.client.session.ui.attachment.AttachmentEditorKindTest`.
|
||||
- From `packages/kilo-jetbrains/`, run `./gradlew typecheck`.
|
||||
- If the targeted Gradle test selector is not accepted, run the nearest frontend test task or `./gradlew test` from `packages/kilo-jetbrains/`.
|
||||
+22
-4
@@ -105,6 +105,7 @@ class PromptPanel(
|
||||
private var bus: MessageBusConnection? = null
|
||||
private var autoApprove = false
|
||||
private var attachment = true
|
||||
private var submitting = false
|
||||
|
||||
private val editor = PromptEditorTextField(project, this).apply {
|
||||
border = JBUI.Borders.empty()
|
||||
@@ -182,7 +183,7 @@ class PromptPanel(
|
||||
private var request = 0L
|
||||
|
||||
override val isSendEnabled: Boolean
|
||||
get() = ready && !busy && (text().isNotEmpty() || attachments.isNotEmpty())
|
||||
get() = ready && !busy && !submitting && (text().isNotEmpty() || attachments.isNotEmpty())
|
||||
|
||||
override val isStopEnabled: Boolean
|
||||
get() = busy
|
||||
@@ -393,9 +394,26 @@ class PromptPanel(
|
||||
private fun submit(src: String) {
|
||||
if (!isSendEnabled) return
|
||||
val txt = text()
|
||||
val files = attachments.map { it.part() }
|
||||
LOG.debug { "${ChatLogSummary.prompt(promptDto(txt, files))} src=$src busy=$busy" }
|
||||
onSend(txt, files)
|
||||
val items = attachments.toList()
|
||||
submitting = true
|
||||
ApplicationManager.getApplication().executeOnPooledThread {
|
||||
try {
|
||||
val files = items.map { it.part() }
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
submitting = false
|
||||
if (project.isDisposed) return@invokeLater
|
||||
LOG.debug { "${ChatLogSummary.prompt(promptDto(txt, files))} src=$src busy=$busy" }
|
||||
onSend(txt, files)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
submitting = false
|
||||
if (project.isDisposed) return@invokeLater
|
||||
LOG.warn("kind=prompt-submit src=$src failed message=${e.message}", e)
|
||||
notify(KiloBundle.message("prompt.attachment.send.failed", e.message ?: e.javaClass.simpleName))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
|
||||
@@ -143,6 +143,7 @@ prompt.attachment.tooltip=Name: {0}\nType: {1}\nLocation: {2}
|
||||
prompt.attachment.embedded=Embedded content
|
||||
prompt.attachment.unsupported.model=The selected model does not support image or PDF attachments.
|
||||
prompt.attachment.missing=Attachment no longer exists: {0}
|
||||
prompt.attachment.send.failed=Failed to send attachment: {0}
|
||||
session.attachment.title=Attachment
|
||||
session.attachment.path=Kilo / Attachments / {0} / {1}
|
||||
session.attachment.loading=Loading attachment...
|
||||
|
||||
+10
@@ -122,6 +122,7 @@ class PromptPanelTest : BasePlatformTestCase() {
|
||||
|
||||
panel.addAttachmentForTest(PromptAttachment("a", "a.png", "image/png", "file:///tmp/a.png"))
|
||||
panel.send()
|
||||
waitForSend { sent }
|
||||
|
||||
assertTrue(sent)
|
||||
}
|
||||
@@ -332,6 +333,7 @@ class PromptPanelTest : BasePlatformTestCase() {
|
||||
PlatformTestUtil.waitForFuture(panel.processPasteForTest(FileListTransferable(listOf(file))))
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
panel.send()
|
||||
waitForSend { sent != null }
|
||||
|
||||
val item = sent!!
|
||||
assertEquals("text/plain", item.mime)
|
||||
@@ -611,6 +613,14 @@ class PromptPanelTest : BasePlatformTestCase() {
|
||||
return factory.createEditor(factory.createDocument(""), project)
|
||||
}
|
||||
|
||||
private fun waitForSend(done: () -> Boolean) {
|
||||
repeat(50) {
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
if (done()) return
|
||||
Thread.sleep(20)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pasteContext(editor: Editor, item: Transferable) = DataContext { id ->
|
||||
when (id) {
|
||||
CommonDataKeys.EDITOR.name -> editor
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
package ai.kilocode.cli
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
object KiloCliParser {
|
||||
private val tags = ConcurrentHashMap<String, Regex>()
|
||||
|
||||
fun tag(text: String, name: String): String? =
|
||||
Regex("<$name>\\s*([\\s\\S]*?)\\s*</$name>")
|
||||
tags.computeIfAbsent(name) {
|
||||
val tag = Regex.escape(it)
|
||||
Regex("<$tag>\\s*([\\s\\S]*?)\\s*</$tag>")
|
||||
}
|
||||
.find(text)
|
||||
?.groupValues
|
||||
?.getOrNull(1)
|
||||
|
||||
Reference in New Issue
Block a user