fix(jetbrains): remove prompt auto-approve

This commit is contained in:
kirillk
2026-05-22 14:03:32 -04:00
parent c4d85ca5ff
commit 82b1e52ef9
11 changed files with 1 additions and 316 deletions
@@ -1,32 +0,0 @@
package ai.kilocode.client.app
import com.intellij.openapi.components.Service
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* Application-level service tracking the runtime auto-approve toggle.
*
* This is a **client-side** toggle only. It does NOT write CLI config or
* create persistent permission rules. While enabled, each permission request
* is automatically replied with `"once"`.
*/
@Service(Service.Level.APP)
class KiloAutoApproveService {
private val state = MutableStateFlow(false)
val enabled: StateFlow<Boolean> = state.asStateFlow()
fun active(): Boolean = state.value
fun set(value: Boolean) {
if (state.value == value) return
state.value = value
}
fun toggle(): Boolean {
val next = !state.value
set(next)
return next
}
}
@@ -1,7 +1,6 @@
package ai.kilocode.client.session
import ai.kilocode.client.app.KiloAppService
import ai.kilocode.client.app.KiloAutoApproveService
import ai.kilocode.client.app.KiloSessionService
import ai.kilocode.client.app.Workspace
import ai.kilocode.client.session.model.SessionModelEvent
@@ -33,7 +32,6 @@ import com.intellij.util.ui.JBUI
import ai.kilocode.log.KiloLog
import com.intellij.ide.ui.LafManagerListener
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.colors.EditorColorsListener
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.openapi.Disposable
@@ -74,7 +72,6 @@ class SessionUi(
private val project = project
private val app = app
private val auto = service<KiloAutoApproveService>()
private var opening = ref != null
private var pending = false
private var loaded: Boolean? = null
@@ -93,7 +90,6 @@ class SessionUi(
beforeUpdate = { if (opening) false else scroll.atBottom() },
afterUpdate = { if (!opening) scroll.followBottom(it) },
loaded = ::onSessionLoaded,
auto = auto,
openProfileAction = ::openProfileSettings,
)
@@ -199,8 +195,6 @@ class SessionUi(
project = project,
onSend = { text -> sendPrompt(text) },
onAbort = { controller.abort() },
autoApprove = { auto.active() },
onAutoApproveToggle = { controller.toggleAutoApprove() },
)
sessionContent.add(header, BorderLayout.NORTH)
@@ -300,15 +294,6 @@ class SessionUi(
is SessionModelEvent.Cleared -> Unit
}
}
prompt.setAutoApprove(auto.active())
cs.launch {
auto.enabled.collect { value ->
ApplicationManager.getApplication().invokeLater {
if (!Disposer.isDisposed(this@SessionUi)) prompt.setAutoApprove(value)
}
}
}
}
private fun bindStyle() {
@@ -1,7 +1,6 @@
package ai.kilocode.client.session.controller
import ai.kilocode.client.app.KiloAppService
import ai.kilocode.client.app.KiloAutoApproveService
import ai.kilocode.client.app.KiloSessionService
import ai.kilocode.client.app.Workspace
import ai.kilocode.client.plugin.KiloBundle
@@ -79,7 +78,6 @@ class SessionController(
private val beforeUpdate: () -> Boolean = { false },
private val afterUpdate: (Boolean) -> Unit = {},
private val loaded: (Boolean) -> Unit = {},
private val auto: KiloAutoApproveService? = null,
private val openProfileAction: () -> Unit = {},
) : Disposable {
@@ -378,31 +376,6 @@ class SessionController(
}
}
fun toggleAutoApprove(): Boolean {
assertEdt()
val next = auto?.toggle() ?: false
if (next) drainPermissions()
return next
}
private fun drainPermissions() {
val id = sid ?: return
val tracked = childIds.toSet()
cs.launch {
try {
val all = sessions.pendingPermissions(directory)
val ids = setOf(id) + tracked
val pending = all.filter { it.sessionID in ids }
for (req in pending) {
sessions.replyPermission(req.id, directory, PermissionReplyDto("once"))
}
LOG.debug { "${ChatLogSummary.sid(id)} kind=auto-approve drain count=${pending.size}" }
} catch (e: Exception) {
LOG.warn("${ChatLogSummary.sid(id)} kind=auto-approve drain failed message=${e.message}", e)
}
}
}
init {
(ref as? SessionRef.Local)?.session?.let { model.setSession(it) }
when (val item = ref) {
@@ -654,12 +627,6 @@ class SessionController(
val permissions = sessions.pendingPermissions(directory).filter { it.sessionID == child }
if (permissions.isEmpty()) return
LOG.debug { "${ChatLogSummary.sid(sid ?: "pending")} kind=child-recovery child=$child permissions=${permissions.size}" }
if (auto?.active() == true) {
for (req in permissions) {
sessions.replyPermission(req.id, directory, PermissionReplyDto("once"))
}
return
}
val last = toPermission(permissions.last())
runEdt {
if (disposed) return@runEdt
@@ -687,12 +654,6 @@ class SessionController(
LOG.debug {
"${ChatLogSummary.sid(id)} kind=recovery permissions=${permissions.size} questions=${questions.size} status=${status?.type ?: "none"} branch=$branch"
}
if (permissions.isNotEmpty() && auto?.active() == true) {
for (req in permissions) {
sessions.replyPermission(req.id, directory, PermissionReplyDto("once"))
}
return
}
runEdt {
if (disposed) return@runEdt
if (sid != id) return@runEdt
@@ -802,10 +763,6 @@ class SessionController(
is ChatEventDto.PermissionAsked -> {
val perm = toPermission(event.request)
if (auto?.active() == true) {
replyPermission(perm.id, PermissionReplyDto("once"))
return
}
model.setState(SessionState.AwaitingPermission(perm))
}
@@ -62,15 +62,12 @@ class PromptPanel(
private val project: Project,
private val onSend: (String) -> Unit,
private val onAbort: () -> Unit,
private val autoApprove: () -> Boolean = { false },
private val onAutoApproveToggle: () -> Boolean = { false },
) : BorderLayoutPanel(), SessionEditorStyleTarget, SendPromptContext {
companion object {
private val LOG = KiloLog.create(PromptPanel::class.java)
private val SEND_ICON: Icon = IconLoader.getIcon("/icons/send.svg", PromptPanel::class.java)
private val STOP_ICON: Icon = IconLoader.getIcon("/icons/stop.svg", PromptPanel::class.java)
private val SHIELD_ICON: Icon = IconLoader.getIcon("/icons/shield.svg", PromptPanel::class.java)
}
val mode = ModePicker()
@@ -134,13 +131,6 @@ class PromptPanel(
addActionListener { onReset() }
}
private val autoIcon = HoverIcon().apply {
icon = SHIELD_ICON
addActionListener {
setAutoApprove(onAutoApproveToggle())
}
}
@Volatile
private var busy = false
private var ready = false
@@ -163,7 +153,6 @@ class PromptPanel(
)
applyStyle(style)
setAutoApprove(autoApprove())
shell.add(editor, BorderLayout.CENTER)
val bar = BorderLayoutPanel().apply {
@@ -179,8 +168,6 @@ class PromptPanel(
bar.add(Box.createHorizontalStrut(JBUI.scale(SessionUiStyle.View.Prompt.CONTROL_GAP)))
bar.add(reset)
bar.add(Box.createHorizontalGlue())
bar.add(autoIcon)
bar.add(Box.createHorizontalStrut(JBUI.scale(SessionUiStyle.View.Prompt.CONTROL_GAP)))
bar.add(button)
shell.add(bar, BorderLayout.SOUTH)
add(shell, BorderLayout.CENTER)
@@ -224,8 +211,6 @@ class PromptPanel(
internal fun buttonForTest(): JButton = button
internal fun autoApproveButtonForTest(): JButton = autoIcon
internal val defaultFocusedComponent: JComponent get() = editor
override fun applyStyle(style: SessionEditorStyle) {
@@ -249,21 +234,6 @@ class PromptPanel(
editor.requestFocusInWindow()
}
fun setAutoApprove(value: Boolean) {
autoIcon.putClientProperty("selected", value)
autoIcon.toolTipText = if (value) {
KiloBundle.message("prompt.autoApprove.enabled")
} else {
KiloBundle.message("prompt.autoApprove.disabled")
}
autoIcon.accessibleContext.accessibleName = if (value) {
KiloBundle.message("prompt.autoApprove.disable")
} else {
KiloBundle.message("prompt.autoApprove.enable")
}
autoIcon.repaint()
}
override fun addNotify() {
super.addNotify()
bindKeymap()
@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path fill="#6E6E6E" d="M8 1L2 3.5v4C2 11.1 4.7 14.1 8 15c3.3-.9 6-3.9 6-7.5v-4L8 1zm0 6.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0 3c-1.7 0-2-.75-2-.75V10c0-.55 1-1 2-1s2 .45 2 1v-.25S9.7 10.5 8 10.5z"/>
</svg>

Before

Width:  |  Height:  |  Size: 308 B

@@ -46,10 +46,6 @@ 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.
session.question.dismiss=Dismiss
session.question.submit=Submit
session.question.next=Next
@@ -1,26 +0,0 @@
package ai.kilocode.client.app
import com.intellij.ide.util.PropertiesComponent
import com.intellij.testFramework.fixtures.BasePlatformTestCase
@Suppress("UnstableApiUsage")
class KiloAutoApproveServiceTest : BasePlatformTestCase() {
fun `test auto approve starts disabled even with stale persisted value`() {
PropertiesComponent.getInstance().setValue("kilo.permission.autoApprove.enabled", "true")
val svc = KiloAutoApproveService()
assertFalse(svc.active())
PropertiesComponent.getInstance().unsetValue("kilo.permission.autoApprove.enabled")
}
fun `test toggle is runtime only`() {
val svc = KiloAutoApproveService()
assertTrue(svc.toggle())
assertTrue(svc.active())
assertFalse(PropertiesComponent.getInstance().getBoolean("kilo.permission.autoApprove.enabled", false))
}
}
@@ -1,6 +1,5 @@
package ai.kilocode.client.session.controller
import ai.kilocode.client.app.KiloAutoApproveService
import ai.kilocode.client.session.model.PermissionFileDiff
import ai.kilocode.client.session.model.PermissionMeta
import ai.kilocode.client.session.model.SessionState
@@ -213,27 +212,6 @@ class PromptLifecycleTest : SessionControllerTestBase() {
assertEquals(1, rpc.permissionReplies.size)
}
fun `test auto-approve live event replies once without showing prompt`() {
appRpc.state.value = ai.kilocode.rpc.dto.KiloAppStateDto(
ai.kilocode.rpc.dto.KiloAppStatusDto.READY,
config = ai.kilocode.rpc.dto.ConfigDto(model = "kilo/gpt-5"),
)
projectRpc.state.value = workspaceReady()
val svc = KiloAutoApproveService()
svc.set(true)
val m = controller(flushMs = Long.MAX_VALUE, auto = svc)
edt { m.prompt("go") }
flush()
emit(ChatEventDto.PermissionAsked("ses_test", permission("perm_auto")))
assertFalse(m.model.state is SessionState.AwaitingPermission)
assertEquals(1, rpc.permissionReplies.size)
assertEquals("once", rpc.permissionReplies[0].third.reply)
svc.set(false)
}
// ------ Child session (subagent) permission bubbling ------
fun `test task part with child sessionId causes controller to track child`() {
@@ -315,28 +293,6 @@ class PromptLifecycleTest : SessionControllerTestBase() {
assertTrue("Root state must not be changed by child non-permission events", stateEvents.isEmpty())
}
fun `test child permission with auto-approve replies once without showing prompt`() {
appRpc.state.value = ai.kilocode.rpc.dto.KiloAppStateDto(
ai.kilocode.rpc.dto.KiloAppStatusDto.READY,
config = ai.kilocode.rpc.dto.ConfigDto(model = "kilo/gpt-5"),
)
projectRpc.state.value = workspaceReady()
val svc = KiloAutoApproveService()
svc.set(true)
val m = controller(flushMs = Long.MAX_VALUE, auto = svc)
edt { m.prompt("go") }
flush()
emit(taskPart("ses_child"), flush = false)
emit(ChatEventDto.PermissionAsked("ses_child", childPermission("child_auto")))
assertFalse(m.model.state is SessionState.AwaitingPermission)
assertEquals(1, rpc.permissionReplies.size)
assertEquals("once", rpc.permissionReplies[0].third.reply)
svc.set(false)
}
fun `test root permission event is not processed as child permission`() {
val (m, _, _) = prompted()
@@ -1,7 +1,6 @@
package ai.kilocode.client.session.controller
import ai.kilocode.client.app.KiloAppService
import ai.kilocode.client.app.KiloAutoApproveService
import ai.kilocode.client.app.KiloSessionService
import ai.kilocode.client.session.model.SessionModel
import ai.kilocode.client.session.model.SessionModelEvent
@@ -129,9 +128,8 @@ abstract class SessionControllerTestBase : BasePlatformTestCase() {
id: String? = null,
flushMs: Long = Long.MAX_VALUE,
displayMs: Long = Long.MAX_VALUE,
auto: KiloAutoApproveService? = null,
): SessionController {
return controller(id, flushMs, true, displayMs = displayMs, auto = auto)
return controller(id, flushMs, true, displayMs = displayMs)
}
protected fun controller(
@@ -151,7 +149,6 @@ abstract class SessionControllerTestBase : BasePlatformTestCase() {
beforeUpdate: () -> Boolean = { false },
afterUpdate: (Boolean) -> Unit = {},
ref: SessionRef? = if (session != null) SessionRef.Local(session) else SessionRef.from(id),
auto: KiloAutoApproveService? = null,
): SessionController {
val root = Root()
val m = SessionController(
@@ -167,7 +164,6 @@ abstract class SessionControllerTestBase : BasePlatformTestCase() {
displayMs,
beforeUpdate = beforeUpdate,
afterUpdate = afterUpdate,
auto = auto,
)
controllers.add(m)
roots[m] = root
@@ -1,6 +1,5 @@
package ai.kilocode.client.session.controller
import ai.kilocode.client.app.KiloAutoApproveService
import ai.kilocode.client.session.model.SessionState
import ai.kilocode.rpc.dto.MessageWithPartsDto
import ai.kilocode.rpc.dto.PartDto
@@ -236,31 +235,6 @@ class SessionRecoveryTest : SessionControllerTestBase() {
)
}
fun `test auto-approve recovery replies once and does not show awaiting permission`() {
rpc.pendingPermissionList.add(
PermissionRequestDto(
id = "perm_auto",
sessionID = "ses_test",
permission = "read",
patterns = listOf("*.json"),
)
)
val svc = KiloAutoApproveService()
svc.set(true)
appRpc.state.value = ai.kilocode.rpc.dto.KiloAppStateDto(ai.kilocode.rpc.dto.KiloAppStatusDto.READY, config = ai.kilocode.rpc.dto.ConfigDto(model = "kilo/gpt-5"))
projectRpc.state.value = workspaceReady()
val m = controller("ses_test", auto = svc)
flush()
assertFalse(m.model.state is SessionState.AwaitingPermission)
assertEquals(1, rpc.permissionReplies.size)
assertEquals("once", rpc.permissionReplies[0].third.reply)
svc.set(false)
}
// ------ Child session permission recovery from history ------
fun `test history with task part and pending child permission recovers to AwaitingPermission`() {
@@ -299,46 +273,6 @@ class SessionRecoveryTest : SessionControllerTestBase() {
assertEquals("ses_child", perm.sessionId)
}
fun `test auto-approve child permission recovery replies once without showing prompt`() {
rpc.history.add(
MessageWithPartsDto(
info = msg("msg1", "ses_test", "assistant"),
parts = listOf(
PartDto(
id = "part_task",
sessionID = "ses_test",
messageID = "msg1",
type = "tool",
tool = "task",
metadata = mapOf("sessionId" to "ses_child"),
),
),
)
)
rpc.pendingPermissionList.add(
PermissionRequestDto(
id = "child_perm_auto",
sessionID = "ses_child",
permission = "edit",
patterns = listOf("*.kt"),
)
)
val svc = KiloAutoApproveService()
svc.set(true)
appRpc.state.value = ai.kilocode.rpc.dto.KiloAppStateDto(ai.kilocode.rpc.dto.KiloAppStatusDto.READY)
projectRpc.state.value = workspaceReady()
val m = controller("ses_test", auto = svc)
flush()
assertFalse(m.model.state is SessionState.AwaitingPermission)
assertEquals(1, rpc.permissionReplies.size)
assertEquals("once", rpc.permissionReplies[0].third.reply)
svc.set(false)
}
fun `test pending child permission from unrelated session is ignored`() {
rpc.pendingPermissionList.add(
PermissionRequestDto(
@@ -120,54 +120,6 @@ class PromptPanelTest : BasePlatformTestCase() {
assertSame(shell, panel.mode.parent.parent)
}
fun `test auto-approve button initializes with disabled tooltip`() {
val panel = PromptPanel(project, {}, {}, autoApprove = { false }, onAutoApproveToggle = { true })
assertEquals(
"Auto-approve disabled. Click to auto-approve permission requests.",
panel.autoApproveButtonForTest().toolTipText,
)
}
fun `test clicking auto-approve button toggles state`() {
var enabled = false
val panel = PromptPanel(
project, {}, {},
autoApprove = { enabled },
onAutoApproveToggle = { enabled = !enabled; enabled },
)
panel.autoApproveButtonForTest().doClick()
assertEquals(
"Auto-approve enabled. Permission requests will be approved once automatically.",
panel.autoApproveButtonForTest().toolTipText,
)
}
fun `test setAutoApprove updates tooltip to enabled`() {
val panel = PromptPanel(project, {}, {})
panel.setAutoApprove(true)
assertEquals(
"Auto-approve enabled. Permission requests will be approved once automatically.",
panel.autoApproveButtonForTest().toolTipText,
)
}
fun `test setAutoApprove updates tooltip to disabled`() {
val panel = PromptPanel(project, {}, {})
panel.setAutoApprove(true)
panel.setAutoApprove(false)
assertEquals(
"Auto-approve disabled. Click to auto-approve permission requests.",
panel.autoApproveButtonForTest().toolTipText,
)
}
private class TestSink : DataSink {
var send: Any? = null