From c4d85ca5ff6faaf94d00158bbcc4f557a1211bfe Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 22 May 2026 13:53:43 -0400 Subject: [PATCH] refactor(jetbrains): move Permission views to views.permission, Align to ui.layout --- .kilo/plans/1779201984530-curious-rocket.md | 1167 +++++++++++++++++ .kilo/plans/1779228770810-stellar-panda.md | 88 ++ .kilo/plans/1779321031193-witty-island.md | 66 + .kilo/plans/1779385276828-misty-garden.md | 91 ++ .kilo/plans/1779387267038-sunny-pixel.md | 88 ++ .kilo/plans/1779392475600-misty-engine.md | 557 ++++++++ .kilo/plans/1779394430104-stellar-falcon.md | 51 + .kilo/plans/1779467130119-kind-squid.md | 163 +++ .../ai/kilocode/client/session/SessionUi.kt | 2 +- .../client/session/ui/EmptySessionPanel.kt | 8 +- .../session/ui/SessionMessageListPanel.kt | 2 +- .../{ => permission}/PermissionDiffView.kt | 2 +- .../views/{ => permission}/PermissionView.kt | 8 +- .../kilocode/client/ui/{ => layout}/Align.kt | 2 +- .../client/session/SessionUiLayoutTest.kt | 2 +- .../session/ui/SessionMessageListPanelTest.kt | 2 +- .../{ => permission}/PermissionViewTest.kt | 2 +- .../client/ui/{ => layout}/AlignTest.kt | 2 +- 18 files changed, 2287 insertions(+), 16 deletions(-) create mode 100644 .kilo/plans/1779201984530-curious-rocket.md create mode 100644 .kilo/plans/1779228770810-stellar-panda.md create mode 100644 .kilo/plans/1779321031193-witty-island.md create mode 100644 .kilo/plans/1779385276828-misty-garden.md create mode 100644 .kilo/plans/1779387267038-sunny-pixel.md create mode 100644 .kilo/plans/1779392475600-misty-engine.md create mode 100644 .kilo/plans/1779394430104-stellar-falcon.md create mode 100644 .kilo/plans/1779467130119-kind-squid.md rename packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/{ => permission}/PermissionDiffView.kt (96%) rename packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/{ => permission}/PermissionView.kt (98%) rename packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/{ => layout}/Align.kt (99%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/{ => permission}/PermissionViewTest.kt (99%) rename packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/{ => layout}/AlignTest.kt (99%) diff --git a/.kilo/plans/1779201984530-curious-rocket.md b/.kilo/plans/1779201984530-curious-rocket.md new file mode 100644 index 00000000000..022167a9b19 --- /dev/null +++ b/.kilo/plans/1779201984530-curious-rocket.md @@ -0,0 +1,1167 @@ +# Permission Views For JetBrains — VS Code Parity Implementation Plan + +## What To Build + +Implement JetBrains permission prompt parity with VS Code for the current request only: + +- Rich permission card in the JetBrains session transcript. +- `Run` / allow once and `Deny` / reject actions. +- Runtime auto-approve enabled toggle that replies `once` automatically while enabled. +- Command and diff previews for permission requests. + +Explicitly do not implement persistent auto-approved rule editing in this pass: + +- No “Manage Auto-Approve Rules” section. +- No per-rule allow/deny toggles in the permission prompt. +- No permission settings tab/editor work. +- No new `PermissionAlwaysRulesDto` calls from the prompt UI. +- Do not surface full `config.permission` just to prefill rule states. + +## Required Context Before Editing + +Read first: + +- `packages/kilo-jetbrains/AGENTS.md` + - Use standard Swing/IntelliJ UI components only. + - Do not add Kotlin UI DSL, Compose, or JCEF. + - All Swing UI mutation must happen on EDT. + - Add user-facing strings to bundle files. +- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt` +- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt` +- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PermissionView.kt` +- `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt` +- `packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt` +- `packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt` + +VS Code reference files: + +- `packages/kilo-vscode/webview-ui/src/components/chat/PermissionDock.tsx` +- `packages/kilo-vscode/webview-ui/src/components/chat/permission-dock-utils.ts` +- `packages/kilo-vscode/webview-ui/src/components/chat/permission-diff-utils.ts` +- `packages/kilo-vscode/src/commands/toggle-auto-approve.ts` + +## Current JetBrains Architecture Summary + +The JetBrains plugin is split-mode: + +- `shared/`: serializable RPC contracts and DTOs. +- `backend/`: CLI process, HTTP/SSE parsing, backend RPC implementations. +- `frontend/`: Swing UI, session controller/model/view. + +Current permission path: + +1. CLI emits `permission.asked` over SSE. +2. `KiloBackendChatManager` receives the SSE event and calls `KiloCliDataParser.parseChatEvent(...)`. +3. `KiloCliDataParser.parsePermissionRequest(...)` creates `PermissionRequestDto`. +4. `KiloSessionRpcApiImpl.events(...)` exposes filtered session events to frontend. +5. `KiloSessionService.events(...)` collects those RPC events. +6. `SessionController.handle(PermissionAsked)` calls `model.setState(SessionState.AwaitingPermission(toPermission(event.request)))`. +7. `SessionMessageListPanel` shows `PermissionView` when state is `AwaitingPermission`. +8. `PermissionView` currently shows minimal UI and sends `PermissionReplyDto("once")` or `PermissionReplyDto("reject")`. +9. `SessionController.replyPermission(...)` calls RPC; backend posts to `/permission/{requestId}/reply?directory=...`. + +Current gaps: + +- `PermissionView` uses Kotlin UI DSL; new work should replace it with hand-built Swing. +- Nested metadata (`filediff`, `files`, `rules`) is flattened or lost. +- No command-specific card UI. +- No diff preview. +- No runtime auto-approve enabled toggle. +- No responding/error state in the view. + +## VS Code Behavior To Match Now + +Match these behaviors: + +- Permission card appears above the prompt input, anchored at the end of the transcript. +- `Run` approves the current request once by replying `"once"`. +- `Deny` rejects the current request by replying `"reject"`. +- Bash permission shows the full command. +- Non-bash permission shows a readable tool/pattern summary. +- Edit/patch/write permission shows diff preview when metadata includes diff information. +- Prompt input is busy/blocked while permission is pending via existing `SessionState.isBusy()` behavior. +- Runtime auto-approve toggle lives near prompt actions and, when enabled, automatically replies `"once"` to pending/future permissions. + +Do not match these VS Code behaviors yet: + +- Persistent rule controls inside `PermissionDock`. +- Settings UI for permissions. +- Saved rule preselection from `config.permission`. +- Open diff in a new tab unless there is already an easy existing JetBrains utility. +- Full subagent permission-family queuing unless it is trivial in existing JetBrains session model. + +## Important Concept Distinction + +There are two “auto approve” concepts: + +1. Persistent auto-approved rules: + - Stored in CLI config under `permission`. + - Can allow/ask/deny future matching requests. + - VS Code edits these from settings and optionally from the prompt. + - Out of scope. + +2. Runtime auto-approve enabled: + - Client-side toggle. + - Does not write CLI config. + - Replies `"once"` to each request while enabled. + - Should be implemented. + +Use copy that makes this distinction obvious. Do not describe runtime auto-approve as “always allow” or “save rule”. + +## Implementation Sequence + +Follow this order. It keeps each step buildable and testable. + +1. Add richer permission DTO fields in `shared`. +2. Parse richer permission metadata in backend parser. +3. Map DTO fields into frontend permission model. +4. Rewrite `PermissionView` as Swing with command/pattern/diff display and allow/deny only. +5. Wire responding/error state only if it is small and testable. +6. Add runtime auto-approve service/state. +7. Add prompt shield toggle. +8. Integrate auto-approve into live permission and recovery flows. +9. Add/adjust tests. +10. Run targeted verification. + +## Step 1 — Extend Shared Permission DTOs + +File: `packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt` + +Add a serializable DTO near existing permission DTOs: + +```kotlin +@Serializable +data class PermissionFileDiffDto( + val file: String, + val patch: String? = null, + val before: String? = null, + val after: String? = null, + val additions: Int = 0, + val deletions: Int = 0, +) +``` + +Extend `PermissionRequestDto` with defaulted fields so old JSON remains compatible: + +```kotlin +@Serializable +data class PermissionRequestDto( + val id: String, + val sessionID: String, + val permission: String, + val patterns: List, + val metadata: Map = emptyMap(), + val always: List = emptyList(), + val tool: ToolRefDto? = null, + val message: String? = null, + val command: String? = null, + val rules: List = emptyList(), + val filePath: String? = null, + val fileDiffs: List = emptyList(), +) +``` + +Notes: + +- Keep old constructor call sites compiling by adding new fields at the end with defaults. +- `rules` is display-only in this pass. Do not build toggles from it. +- `always` remains present but the UI should not use it for rule management in this pass. + +## Step 2 — Parse Rich Permission Metadata + +File: `packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt` + +Update imports with `PermissionFileDiffDto`. + +Replace current `parsePermissionRequest(...)` logic with a version that: + +- Reads `id`, `sessionID`, `permission`, `patterns`, `always`, `tool` as today. +- Keeps scalar metadata in `metadata: Map`. +- Extracts optional `message`, `command`, `rules`, `filePath`, and `fileDiffs`. +- Never throws for malformed optional metadata. + +Suggested helper functions to add near JSON helpers: + +```kotlin +private fun JsonElement?.obj(): JsonObject? = runCatching { this?.jsonObject }.getOrNull() +private fun JsonElement?.arr(): JsonArray? = runCatching { this?.jsonArray }.getOrNull() +private fun JsonElement?.text(): String? = this?.scalar() +``` + +Suggested extraction behavior: + +- `message`: `obj.str("message") ?: meta.str("message")` +- `command`: `meta.str("command") ?: obj.str("command")` +- `filePath`: `meta.str("filepath") ?: meta.str("filePath") ?: meta.str("file") ?: meta.str("path")` +- `rules`: support all of these: + - metadata `rules` as JSON array + - metadata `rules` as a single string + - metadata `rules` as JSON-encoded string array if encountered +- `fileDiffs`: support VS Code order: + 1. `metadata.filediff` object + 2. `metadata.files` array + 3. `metadata.diff` + `metadata.filepath` + +Important current parser behavior: + +- Existing `JsonElement.scalar()` already turns arrays/objects into `toString()` if not primitive. +- For rich parsing, use the raw `JsonObject` before flattening to `Map`. + +Suggested `parsePermissionRequest(...)` outline: + +```kotlin +internal fun parsePermissionRequest(obj: JsonObject): PermissionRequestDto? { + val id = obj.str("id") ?: return null + val sid = obj.str("sessionID") ?: return null + val permission = obj.str("permission") ?: return null + val patterns = obj["patterns"]?.jsonArray?.mapNotNull { it.jsonPrimitive.contentOrNull } ?: emptyList() + val always = obj["always"]?.jsonArray?.mapNotNull { it.jsonPrimitive.contentOrNull } ?: emptyList() + val metaObj = obj["metadata"].obj() + val meta = metaObj?.entries?.mapNotNull { (key, value) -> + val text = value.scalar() ?: return@mapNotNull null + key to text + }?.toMap() ?: emptyMap() + val path = metaObj.path() + val diffs = metaObj.permissionDiffs(path) + return PermissionRequestDto( + id = id, + sessionID = sid, + permission = permission, + patterns = patterns, + metadata = meta, + always = always, + tool = toolRef(obj), + message = obj.str("message") ?: metaObj?.str("message"), + command = metaObj?.str("command") ?: obj.str("command"), + rules = metaObj.rules(), + filePath = path, + fileDiffs = diffs, + ) +} +``` + +Helper details: + +- `path()` returns first nonblank value from `filepath`, `filePath`, `file`, `path`. +- `rules()` returns `emptyList()` when absent; if the value is an array, map string items; if a primitive string starts with `[`, attempt JSON array parse with the existing `json` instance; otherwise return a one-item list. +- `permissionDiffs(path)`: + - If `filediff` is an object, parse via `diffObj(...)` and return one item. + - If `files` is an array, parse each item with `relativePath ?: filePath ?: file` and `patch/additions/deletions`. + - If `diff` is a string, return `PermissionFileDiffDto(file = path ?: "patch", patch = diff)`. + - Else return `emptyList()`. + +Do not over-engineer counts: + +- If `additions` or `deletions` is missing/non-numeric, use `0`. +- If both `before` and `after` exist but no patch exists, keep them for possible future display, but UI can ignore them for now. + +## Step 3 — Extend Frontend Permission Model + +File: `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/Permission.kt` + +Update `PermissionMeta`: + +```kotlin +data class PermissionMeta( + val command: String? = null, + val rules: List = emptyList(), + val diff: String? = null, + val filePath: String? = null, + val fileDiff: PermissionFileDiff? = null, + val fileDiffs: List = emptyList(), + val raw: Map = emptyMap(), +) +``` + +Compatibility note: + +- Keep existing `diff`, `fileDiff`, and `raw` fields if tests or callers rely on them. +- Add `fileDiffs`; optionally set `fileDiff = fileDiffs.firstOrNull()` in mapping for compatibility. + +File: `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt` + +Update `toPermission(dto)`: + +- `file = dto.filePath ?: dto.metadata["filepath"] ?: dto.metadata["filePath"] ?: dto.metadata["file"] ?: dto.metadata["path"]` +- `diffs = dto.fileDiffs.map { PermissionFileDiff(...) }` +- `diff = dto.metadata["diff"]` +- `command = dto.command ?: dto.metadata["command"]` +- `rules = dto.rules.ifEmpty { parse legacy metadata rules if desired }` +- `message = dto.message ?: dto.metadata["message"]` + +Suggested mapping: + +```kotlin +val diffs = dto.fileDiffs.map { + PermissionFileDiff( + file = it.file, + patch = it.patch, + before = it.before, + after = it.after, + additions = it.additions, + deletions = it.deletions, + ) +} +val file = dto.filePath ?: dto.metadata["filepath"] ?: dto.metadata["filePath"] ?: dto.metadata["file"] ?: dto.metadata["path"] +return Permission( + id = dto.id, + sessionId = dto.sessionID, + name = dto.permission, + patterns = dto.patterns, + always = dto.always, + meta = PermissionMeta( + command = dto.command ?: dto.metadata["command"], + rules = dto.rules, + diff = dto.metadata["diff"], + filePath = file, + fileDiff = diffs.firstOrNull(), + fileDiffs = diffs, + raw = dto.metadata, + ), + message = dto.message ?: dto.metadata["message"], + tool = ref, + state = state, +) +``` + +## Step 4 — Add Permission UI Strings + +File: `packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties` + +Add/adjust English strings: + +```properties +session.permission.title=Permission required +session.permission.title.subagent=Permission required (subagent) +session.permission.meta=Tool: {0} • Patterns: {1} +session.permission.run=Run +session.permission.allow=Allow +session.permission.deny=Deny +session.permission.command=Command +session.permission.patterns={0}: +session.permission.diff=Changes +session.permission.diff.summary=+{0} -{1} +session.permission.no.details={0} requires permission. +session.permission.responding=Sending response... +session.permission.error=Failed to send permission response +session.permission.tool.read=Read +session.permission.tool.edit=Edit +session.permission.tool.write=Write +session.permission.tool.patch=Patch +session.permission.tool.multiedit=Edit +session.permission.tool.glob=Glob Search +session.permission.tool.grep=Grep Search +session.permission.tool.list=List +session.permission.tool.bash=Shell +session.permission.tool.external_directory=External Directory +session.permission.tool.webfetch=Web Fetch +session.permission.tool.websearch=Web Search +session.permission.tool.codesearch=Code Search +session.permission.tool.todoread=Read Todo List +session.permission.tool.todowrite=Update Todo List +session.permission.tool.task=Task +session.permission.tool.skill=Skill +session.permission.tool.lsp=Language Server +prompt.autoApprove.enable=Enable auto-approve +prompt.autoApprove.disable=Disable auto-approve +prompt.autoApprove.enabled=Auto-approve enabled. Permission requests will be approved once automatically. +prompt.autoApprove.disabled=Auto-approve disabled. Click to auto-approve permission requests. +``` + +Localization decision: + +- If this repo expects all locale files to contain every key, add English fallback values to all `KiloBundle_*.properties` files. +- If missing locale keys fall back to root bundle, updating only `KiloBundle.properties` is acceptable for this pass. + +## Step 5 — Rewrite `PermissionView` As Swing + +File: `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PermissionView.kt` + +Replace UI DSL imports and usage: + +Remove: + +- `com.intellij.ui.dsl.builder.RightGap` +- `com.intellij.ui.dsl.builder.RowLayout` +- `com.intellij.ui.dsl.builder.panel` + +Use platform/Swing components: + +- `JBLabel` +- `JBTextArea` +- `JBScrollPane` +- `BorderLayoutPanel` +- `JPanel` +- `JButton` +- `BoxLayout` +- `FlowLayout` +- `JBUI` +- `UIUtil` or `JBUI.CurrentTheme` for colors. + +Constructor should support replying with optional state updates. Recommended signature: + +```kotlin +class PermissionView( + private val reply: (String, PermissionReplyDto) -> Unit, +) : BorderLayoutPanel(), SessionEditorStyleTarget, SessionView +``` + +If implementing responding state from the view, keep local buttons disabled after click. A more complete approach is in Step 6. + +Suggested class fields: + +```kotlin +private var requestId: String? = null +private var style = SessionEditorStyle.current() +private val card = BorderLayoutPanel() +private val header = JBLabel() +private val details = JPanel() +private val actions = JPanel(FlowLayout(FlowLayout.LEFT, UiStyle.Gap.sm(), 0)) +private val run = JButton(KiloBundle.message("session.permission.run")) +private val deny = JButton(KiloBundle.message("session.permission.deny")) +``` + +Build a stable tree in `init`: + +- `card` center in this view. +- Header row with warning icon and title. +- `details` vertical panel for current permission content. +- Actions row with Run and Deny. + +`show(permission)` should: + +- Set `requestId`. +- Update header text. +- Clear and rebuild only `details` if easiest; this is acceptable because permission changes are infrequent. Avoid Kotlin UI DSL. +- Add command block if `permission.meta.command` is present or `permission.name == "bash"` and metadata command exists. +- Else add pattern summary. +- Add message if present. +- Add diff section if `permission.meta.fileDiffs` is non-empty, or fallback from `permission.meta.diff`. +- Enable buttons unless `permission.state == RESPONDING` or `RESOLVED`. +- Set visible and refresh. + +`hideView()` should: + +- Clear `requestId`. +- Clear details if desired. +- Set invisible. +- Refresh. + +Action behavior: + +```kotlin +private fun decide(value: String) { + val id = requestId ?: return + setResponding(true) + reply(id, PermissionReplyDto(reply = value)) +} +``` + +Do not hide immediately if implementing responding state. Let `permission.replied` hide via model state transition. If not implementing responding state, current immediate hide is acceptable but less VS Code-like. + +Tool label helper: + +```kotlin +private fun label(tool: String): String = when (tool) { + "read" -> KiloBundle.message("session.permission.tool.read") + "edit" -> KiloBundle.message("session.permission.tool.edit") + "write" -> KiloBundle.message("session.permission.tool.write") + "patch" -> KiloBundle.message("session.permission.tool.patch") + "multiedit" -> KiloBundle.message("session.permission.tool.multiedit") + "glob" -> KiloBundle.message("session.permission.tool.glob") + "grep" -> KiloBundle.message("session.permission.tool.grep") + "list" -> KiloBundle.message("session.permission.tool.list") + "bash" -> KiloBundle.message("session.permission.tool.bash") + "external_directory" -> KiloBundle.message("session.permission.tool.external_directory") + "webfetch" -> KiloBundle.message("session.permission.tool.webfetch") + "websearch" -> KiloBundle.message("session.permission.tool.websearch") + "codesearch" -> KiloBundle.message("session.permission.tool.codesearch") + "todoread" -> KiloBundle.message("session.permission.tool.todoread") + "todowrite" -> KiloBundle.message("session.permission.tool.todowrite") + "task" -> KiloBundle.message("session.permission.tool.task") + "skill" -> KiloBundle.message("session.permission.tool.skill") + "lsp" -> KiloBundle.message("session.permission.tool.lsp") + else -> tool +} +``` + +Pattern display: + +- Filter `patterns` to exclude `"*"`. +- If none, show `session.permission.no.details` with label. +- If one, show `"