mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-30 17:14:40 +08:00
fix(jetbrains): tighten prompt completion routing
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": minor
|
||||
---
|
||||
|
||||
Add `/` slash commands and `@` file/git-changes mentions to the JetBrains chat prompt with native completion.
|
||||
-2
@@ -203,8 +203,6 @@ class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi {
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun terminalOutput(directory: String): String? = null
|
||||
|
||||
override suspend fun gitChanges(directory: String): String? = withContext(Dispatchers.IO) {
|
||||
val base = file(clean(directory) ?: directory) ?: return@withContext null
|
||||
if (!gitAvailable(base)) return@withContext null
|
||||
|
||||
-9
@@ -136,15 +136,6 @@ class KiloWorkspaceService internal constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun terminalOutput(directory: String): String? {
|
||||
return try {
|
||||
call { terminalOutput(directory) }
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("terminal output lookup failed for directory=$directory", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun gitChanges(directory: String): String? {
|
||||
return try {
|
||||
call { gitChanges(directory) }
|
||||
|
||||
+18
-75
@@ -8,6 +8,7 @@ import ai.kilocode.client.migration.KiloMigrationService
|
||||
import ai.kilocode.client.migration.MigrationUiController
|
||||
import ai.kilocode.client.migration.MigrationUiState
|
||||
import ai.kilocode.client.migration.ui.MigrationOverlayPanel
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.model.FileAttachment
|
||||
import ai.kilocode.client.session.model.SessionModelEvent
|
||||
import ai.kilocode.client.session.model.SessionState
|
||||
@@ -20,6 +21,8 @@ import ai.kilocode.client.session.ui.mode.ModePicker
|
||||
import ai.kilocode.client.session.ui.model.ModelPicker
|
||||
import ai.kilocode.client.session.ui.prompt.KiloPromptCompletionProvider
|
||||
import ai.kilocode.client.session.ui.prompt.PromptPanel
|
||||
import ai.kilocode.client.session.ui.prompt.gitChangesPart
|
||||
import ai.kilocode.client.session.ui.prompt.mentionFileParts
|
||||
import ai.kilocode.client.session.ui.account.SessionAccountOverlay
|
||||
import ai.kilocode.client.session.ui.SessionDropOverlay
|
||||
import ai.kilocode.client.session.ui.SessionRootPanel
|
||||
@@ -47,8 +50,6 @@ import ai.kilocode.client.vfs.KiloVfsManager
|
||||
import ai.kilocode.log.ChatLogSummary
|
||||
import ai.kilocode.rpc.dto.PromptDto
|
||||
import ai.kilocode.rpc.dto.PromptPartDto
|
||||
import ai.kilocode.rpc.dto.PartSourceDto
|
||||
import ai.kilocode.rpc.dto.PartSourceTextDto
|
||||
import com.intellij.util.ui.JBUI
|
||||
import ai.kilocode.log.KiloLog
|
||||
import com.intellij.ide.BrowserUtil
|
||||
@@ -67,6 +68,7 @@ import com.intellij.openapi.options.Configurable
|
||||
import com.intellij.openapi.options.ConfigurableWithId
|
||||
import com.intellij.openapi.options.ShowSettingsUtil
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.progress.runBlockingCancellable
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
@@ -74,13 +76,10 @@ import java.util.function.Predicate
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.event.HierarchyEvent
|
||||
import java.net.URI
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Path
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
@@ -171,6 +170,7 @@ class SessionUi(
|
||||
private lateinit var connection: ConnectionPanel
|
||||
|
||||
private lateinit var prompt: PromptPanel
|
||||
private lateinit var completion: KiloPromptCompletionProvider
|
||||
private lateinit var load: LoadingPanel
|
||||
private lateinit var migrationOverlay: MigrationOverlayPanel
|
||||
private var empty: EmptySessionPanel? = null
|
||||
@@ -332,7 +332,7 @@ class SessionUi(
|
||||
scroll = SessionScroll(root, sessionContent, messageBody, blankBody)
|
||||
connection = ConnectionPanel(this, controller)
|
||||
|
||||
val completion = KiloPromptCompletionProvider(
|
||||
completion = KiloPromptCompletionProvider(
|
||||
workspace = workspace,
|
||||
service = workspaces,
|
||||
actions = slashActions(),
|
||||
@@ -590,7 +590,7 @@ class SessionUi(
|
||||
}
|
||||
prompt.clear()
|
||||
val follow = scroll.atBottom()
|
||||
val command = serverCommand(text)
|
||||
val command = completion.serverCommand(text)
|
||||
if (command != null) controller.command(command.first, command.second, files)
|
||||
else controller.prompt(text, files)
|
||||
scroll.followBottom(follow)
|
||||
@@ -599,106 +599,49 @@ class SessionUi(
|
||||
private fun slashActions(): List<KiloPromptCompletionProvider.SlashAction> = listOf(
|
||||
KiloPromptCompletionProvider.SlashAction(
|
||||
"new",
|
||||
ai.kilocode.client.plugin.KiloBundle.message("prompt.slash.new"),
|
||||
KiloBundle.message("prompt.slash.new"),
|
||||
) { manager?.newSession() },
|
||||
KiloPromptCompletionProvider.SlashAction(
|
||||
"sessions",
|
||||
ai.kilocode.client.plugin.KiloBundle.message("prompt.slash.sessions"),
|
||||
KiloBundle.message("prompt.slash.sessions"),
|
||||
listOf("history", "resume"),
|
||||
) { manager?.showHistory() },
|
||||
KiloPromptCompletionProvider.SlashAction(
|
||||
"models",
|
||||
ai.kilocode.client.plugin.KiloBundle.message("prompt.slash.models"),
|
||||
KiloBundle.message("prompt.slash.models"),
|
||||
) { prompt.model.open() },
|
||||
KiloPromptCompletionProvider.SlashAction(
|
||||
"agents",
|
||||
ai.kilocode.client.plugin.KiloBundle.message("prompt.slash.agents"),
|
||||
KiloBundle.message("prompt.slash.agents"),
|
||||
listOf("modes"),
|
||||
) { prompt.mode.open() },
|
||||
KiloPromptCompletionProvider.SlashAction(
|
||||
"variant",
|
||||
ai.kilocode.client.plugin.KiloBundle.message("prompt.slash.variant"),
|
||||
KiloBundle.message("prompt.slash.variant"),
|
||||
listOf("reasoning"),
|
||||
) { prompt.reasoning.open() },
|
||||
KiloPromptCompletionProvider.SlashAction(
|
||||
"compact",
|
||||
ai.kilocode.client.plugin.KiloBundle.message("prompt.slash.compact"),
|
||||
KiloBundle.message("prompt.slash.compact"),
|
||||
listOf("smol"),
|
||||
) { controller.compact() },
|
||||
KiloPromptCompletionProvider.SlashAction(
|
||||
"settings",
|
||||
ai.kilocode.client.plugin.KiloBundle.message("prompt.slash.settings"),
|
||||
KiloBundle.message("prompt.slash.settings"),
|
||||
) { openKiloSettings() },
|
||||
KiloPromptCompletionProvider.SlashAction(
|
||||
"help",
|
||||
ai.kilocode.client.plugin.KiloBundle.message("prompt.slash.help"),
|
||||
KiloBundle.message("prompt.slash.help"),
|
||||
) { BrowserUtil.browse("https://kilo.ai/docs") },
|
||||
)
|
||||
|
||||
private fun clientCommandNames(): Set<String> = setOf(
|
||||
"new",
|
||||
"sessions",
|
||||
"models",
|
||||
"agents",
|
||||
"variant",
|
||||
"compact",
|
||||
"settings",
|
||||
"help",
|
||||
)
|
||||
|
||||
private fun serverCommand(text: String): Pair<String, String>? {
|
||||
val raw = text.trimStart()
|
||||
if (!raw.startsWith('/')) return null
|
||||
val name = raw.drop(1).takeWhile { !it.isWhitespace() }
|
||||
if (name.isBlank()) return null
|
||||
if (name in clientCommandNames()) return null
|
||||
if (workspace.state.value.commands.none { it.name == name }) return null
|
||||
return name to raw.drop(name.length + 1).trimStart()
|
||||
}
|
||||
|
||||
private fun mentionParts(text: String, paths: Set<String>): List<PromptPartDto> = buildList {
|
||||
paths.filter { text.contains("@$it") }.forEach { path ->
|
||||
val token = "@$path"
|
||||
val start = text.indexOf(token).takeIf { it >= 0 } ?: return@forEach
|
||||
val target = runCatching {
|
||||
val item = Path.of(path)
|
||||
if (item.isAbsolute) item else Path.of(workspace.directory).resolve(item).normalize()
|
||||
}.getOrNull() ?: return@forEach
|
||||
add(PromptPartDto(
|
||||
type = "file",
|
||||
mime = "text/plain",
|
||||
url = target.toUri().toString(),
|
||||
filename = target.fileName?.toString(),
|
||||
source = source("file", token, start, path = path),
|
||||
))
|
||||
}
|
||||
val terminal = text.indexOf("@terminal")
|
||||
if (terminal >= 0) {
|
||||
runBlocking { workspaces.terminalOutput(workspace.directory) }
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let { add(dataPart("terminal-output.txt", it, source("resource", "@terminal", terminal, uri = "terminal"))) }
|
||||
}
|
||||
val git = text.indexOf("@git-changes")
|
||||
if (git >= 0) {
|
||||
runBlocking { workspaces.gitChanges(workspace.directory) }
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let { add(dataPart("git-changes.txt", it, source("resource", "@git-changes", git, uri = "git-changes"))) }
|
||||
addAll(mentionFileParts(text, paths, workspace.directory))
|
||||
if (text.contains("@git-changes")) {
|
||||
gitChangesPart(text, runBlockingCancellable { workspaces.gitChanges(workspace.directory) })?.let(::add)
|
||||
}
|
||||
}
|
||||
|
||||
private fun dataPart(name: String, text: String, source: PartSourceDto? = null): PromptPartDto {
|
||||
val data = URLEncoder.encode(text, StandardCharsets.UTF_8).replace("+", "%20")
|
||||
return PromptPartDto(type = "file", mime = "text/plain", url = "data:text/plain;charset=utf-8,$data", filename = name, source = source)
|
||||
}
|
||||
|
||||
private fun source(type: String, token: String, start: Int, path: String? = null, uri: String? = null) = PartSourceDto(
|
||||
type = type,
|
||||
text = PartSourceTextDto(value = token, start = start.toDouble(), end = (start + token.length).toDouble()),
|
||||
path = path,
|
||||
uri = uri,
|
||||
clientName = if (type == "resource") "jetbrains" else null,
|
||||
)
|
||||
|
||||
private fun openFile(path: String) {
|
||||
cs.launch {
|
||||
workspaces.openPath(workspace.directory, path)
|
||||
|
||||
+11
-4
@@ -51,6 +51,16 @@ class KiloPromptCompletionProvider(
|
||||
|
||||
fun clientNames(): Set<String> = actions.mapTo(mutableSetOf()) { it.name }
|
||||
|
||||
fun serverCommand(text: String): Pair<String, String>? {
|
||||
val raw = text.trimStart()
|
||||
if (!raw.startsWith('/')) return null
|
||||
val name = raw.drop(1).takeWhile { !it.isWhitespace() }
|
||||
if (name.isBlank()) return null
|
||||
if (name in clientNames()) return null
|
||||
if (workspace.state.value.commands.none { it.name == name }) return null
|
||||
return name to raw.drop(name.length + 1).trimStart()
|
||||
}
|
||||
|
||||
fun highlights(text: String): List<Highlight> = buildList {
|
||||
val command = text.takeIf { it.startsWith('/') }
|
||||
?.drop(1)
|
||||
@@ -62,7 +72,7 @@ class KiloPromptCompletionProvider(
|
||||
}
|
||||
|
||||
val ranges = mutableListOf<IntRange>()
|
||||
val values = (mentionPaths() + setOf("terminal", "git-changes"))
|
||||
val values = (mentionPaths() + setOf("git-changes"))
|
||||
.filter { it.isNotBlank() }
|
||||
.sortedByDescending { it.length }
|
||||
values.forEach { value ->
|
||||
@@ -118,9 +128,6 @@ class KiloPromptCompletionProvider(
|
||||
if ("git-changes".startsWith(prefix, ignoreCase = true) && search.git) {
|
||||
out.addElement(special("git-changes", KiloBundle.message("prompt.mention.gitChanges")))
|
||||
}
|
||||
if ("terminal".startsWith(prefix, ignoreCase = true) && search.terminal) {
|
||||
out.addElement(special("terminal", KiloBundle.message("prompt.mention.terminal")))
|
||||
}
|
||||
if (search.indexing) {
|
||||
val msg = KiloBundle.message("prompt.mention.indexing")
|
||||
result.addLookupAdvertisement(msg)
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package ai.kilocode.client.session.ui.prompt
|
||||
|
||||
import ai.kilocode.rpc.dto.PartSourceDto
|
||||
import ai.kilocode.rpc.dto.PartSourceTextDto
|
||||
import ai.kilocode.rpc.dto.PromptPartDto
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Path
|
||||
|
||||
fun mentionFileParts(text: String, paths: Set<String>, directory: String): List<PromptPartDto> = buildList {
|
||||
paths.filter { text.contains("@$it") }.forEach { path ->
|
||||
val token = "@$path"
|
||||
val start = text.indexOf(token).takeIf { it >= 0 } ?: return@forEach
|
||||
val target = runCatching {
|
||||
val item = Path.of(path)
|
||||
if (item.isAbsolute) item else Path.of(directory).resolve(item).normalize()
|
||||
}.getOrNull() ?: return@forEach
|
||||
add(PromptPartDto(
|
||||
type = "file",
|
||||
mime = "text/plain",
|
||||
url = target.toUri().toString(),
|
||||
filename = target.fileName?.toString(),
|
||||
source = source("file", token, start, path = path),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fun gitChangesPart(text: String, diff: String?): PromptPartDto? {
|
||||
val raw = "@git-changes"
|
||||
val start = text.indexOf(raw)
|
||||
if (start < 0) return null
|
||||
val end = start + raw.length
|
||||
if (end < text.length && !text[end].isWhitespace()) return null
|
||||
val value = diff?.takeIf { it.isNotBlank() } ?: return null
|
||||
return dataPart("git-changes.txt", value, source("resource", raw, start, uri = "git-changes"))
|
||||
}
|
||||
|
||||
private fun dataPart(name: String, text: String, source: PartSourceDto? = null): PromptPartDto {
|
||||
val data = URLEncoder.encode(text, StandardCharsets.UTF_8).replace("+", "%20")
|
||||
return PromptPartDto(type = "file", mime = "text/plain", url = "data:text/plain;charset=utf-8,$data", filename = name, source = source)
|
||||
}
|
||||
|
||||
private fun source(type: String, token: String, start: Int, path: String? = null, uri: String? = null) = PartSourceDto(
|
||||
type = type,
|
||||
text = PartSourceTextDto(value = token, start = start.toDouble(), end = (start + token.length).toDouble()),
|
||||
path = path,
|
||||
uri = uri,
|
||||
clientName = if (type == "resource") "jetbrains" else null,
|
||||
)
|
||||
+1
@@ -512,6 +512,7 @@ class PromptPanel(
|
||||
}
|
||||
|
||||
private fun showCompletion(ed: com.intellij.openapi.editor.Editor) {
|
||||
// Uses IntelliJ impl/internal completion APIs; revisit on platform upgrades.
|
||||
CodeCompletionHandlerBase.createHandler(CompletionType.BASIC, true, false, true)
|
||||
.invokeCompletion(project, ed, 1)
|
||||
val lookup = LookupManager.getActiveLookup(ed) as? LookupImpl ?: return
|
||||
|
||||
@@ -163,7 +163,6 @@ prompt.action.enhance.failed=Failed to enhance prompt
|
||||
prompt.action.enhance.failed.description=The configured small model could not enhance this prompt.
|
||||
prompt.mention.indexing=Indexing project files. File mentions will be available soon.
|
||||
prompt.mention.gitChanges=Attach current git changes
|
||||
prompt.mention.terminal=Attach terminal output
|
||||
prompt.slash.new=Start a new session
|
||||
prompt.slash.sessions=Open session history
|
||||
prompt.slash.models=Choose a model
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package ai.kilocode.client.session.controller
|
||||
|
||||
import ai.kilocode.client.session.model.SessionState
|
||||
import ai.kilocode.rpc.dto.CommandDto
|
||||
import ai.kilocode.rpc.dto.ConfigDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStateDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStatusDto
|
||||
|
||||
class CommandLifecycleTest : SessionControllerTestBase() {
|
||||
|
||||
fun `test command creates new session and calls RPC`() {
|
||||
appRpc.state.value = KiloAppStateDto(KiloAppStatusDto.READY, config = ConfigDto(model = "kilo/gpt-5"))
|
||||
projectRpc.state.value = workspaceReady().copy(commands = listOf(CommandDto("deploy")))
|
||||
val m = controller()
|
||||
|
||||
flush()
|
||||
edt { m.command("deploy", "prod") }
|
||||
flush()
|
||||
|
||||
assertEquals(1, rpc.creates)
|
||||
assertEquals(1, rpc.commands.size)
|
||||
val call = rpc.commands.single()
|
||||
assertEquals("ses_test", call.id)
|
||||
assertEquals("/test", call.directory)
|
||||
assertEquals("deploy", call.command)
|
||||
assertEquals("prod", call.arguments)
|
||||
}
|
||||
|
||||
fun `test command reuses existing session`() {
|
||||
val (m, _, _) = prompted()
|
||||
val created = rpc.creates
|
||||
|
||||
edt { m.command("deploy", "prod") }
|
||||
flush()
|
||||
|
||||
assertEquals(created, rpc.creates)
|
||||
assertEquals("ses_test", rpc.commands.single().id)
|
||||
}
|
||||
|
||||
fun `test command records telemetry`() {
|
||||
appRpc.state.value = KiloAppStateDto(KiloAppStatusDto.READY, config = ConfigDto(model = "kilo/gpt-5"))
|
||||
projectRpc.state.value = workspaceReady().copy(commands = listOf(CommandDto("deploy")))
|
||||
val m = controller()
|
||||
|
||||
flush()
|
||||
edt { m.command("deploy", "prod") }
|
||||
flush()
|
||||
|
||||
val sent = appRpc.telemetry.single { it.event == "Conversation Send Clicked" }
|
||||
assertEquals("command", sent.properties["source"])
|
||||
val message = appRpc.telemetry.single { it.event == "Conversation Message" }
|
||||
assertEquals("command", message.properties["source"])
|
||||
}
|
||||
|
||||
fun `test command errors set state and telemetry`() {
|
||||
appRpc.state.value = KiloAppStateDto(KiloAppStatusDto.READY, config = ConfigDto(model = "kilo/gpt-5"))
|
||||
projectRpc.state.value = workspaceReady().copy(commands = listOf(CommandDto("deploy")))
|
||||
rpc.commandThrows = IllegalStateException("boom")
|
||||
val m = controller()
|
||||
|
||||
flush()
|
||||
edt { m.command("deploy", "prod") }
|
||||
flush()
|
||||
|
||||
assertTrue(m.model.state is SessionState.Error)
|
||||
val event = appRpc.telemetry.single { it.event == "Session Error" }
|
||||
assertEquals("command", event.properties["context"])
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -221,12 +221,12 @@ class PromptPanelTest : BasePlatformTestCase() {
|
||||
val field = panel.defaultFocusedComponent as EditorTextField
|
||||
|
||||
realize(panel, 260, 400)
|
||||
field.text = "/new use @terminal and @unknown"
|
||||
field.text = "/new use @git-changes and @unknown"
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
|
||||
val spans = spans(field)
|
||||
assertTrue(spans.contains("/new" to DefaultLanguageHighlighterColors.KEYWORD))
|
||||
assertTrue(spans.contains("@terminal" to DefaultLanguageHighlighterColors.METADATA))
|
||||
assertTrue(spans.contains("@git-changes" to DefaultLanguageHighlighterColors.METADATA))
|
||||
assertFalse(spans.any { it.first == "@unknown" })
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ class PromptPanelTest : BasePlatformTestCase() {
|
||||
val field = panel.defaultFocusedComponent as EditorTextField
|
||||
|
||||
realize(panel, 260, 400)
|
||||
field.text = "use @terminal"
|
||||
field.text = "use @git-changes"
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
assertEquals(1, field.getEditor(false)!!.markupModel.allHighlighters.size)
|
||||
|
||||
@@ -251,7 +251,7 @@ class PromptPanelTest : BasePlatformTestCase() {
|
||||
|
||||
realize(panel, 260, 400)
|
||||
repeat(50) {
|
||||
field.text = if (it % 2 == 0) "/new @terminal" else "/new @git-changes"
|
||||
field.text = if (it % 2 == 0) "/new @git-changes" else "/new @git-changes now"
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
assertTrue(field.getEditor(false)!!.markupModel.allHighlighters.size <= 2)
|
||||
}
|
||||
|
||||
+14
-4
@@ -69,7 +69,7 @@ class KiloPromptCompletionProviderTest : BasePlatformTestCase() {
|
||||
}
|
||||
|
||||
fun `test mention completion includes matching special items`() {
|
||||
rpc.searchResult = FileSearchResultDto(git = true, terminal = true)
|
||||
rpc.searchResult = FileSearchResultDto(git = true)
|
||||
|
||||
complete("@git<caret>")
|
||||
|
||||
@@ -103,13 +103,23 @@ class KiloPromptCompletionProviderTest : BasePlatformTestCase() {
|
||||
fun `test highlights special mentions without tracked paths`() {
|
||||
assertEquals(
|
||||
listOf(
|
||||
KiloPromptCompletionProvider.Highlight(4, 13, KiloPromptCompletionProvider.HighlightKind.MENTION),
|
||||
KiloPromptCompletionProvider.Highlight(18, 30, KiloPromptCompletionProvider.HighlightKind.MENTION),
|
||||
KiloPromptCompletionProvider.Highlight(4, 16, KiloPromptCompletionProvider.HighlightKind.MENTION),
|
||||
),
|
||||
provider.highlights("use @terminal and @git-changes").sortedBy { it.start },
|
||||
provider.highlights("use @git-changes").sortedBy { it.start },
|
||||
)
|
||||
}
|
||||
|
||||
fun `test serverCommand routes only known server commands`() {
|
||||
rpc.state.value = KiloWorkspaceStateDto(KiloWorkspaceStatusDto.READY, commands = listOf(CommandDto("deploy")))
|
||||
|
||||
waitFor { provider.serverCommand("/deploy x") != null }
|
||||
|
||||
assertEquals("deploy" to "x", provider.serverCommand("/deploy x"))
|
||||
assertNull(provider.serverCommand("/new"))
|
||||
assertNull(provider.serverCommand("hi /deploy"))
|
||||
assertNull(provider.serverCommand("/unknown"))
|
||||
}
|
||||
|
||||
fun `test highlights tracked mentions longest first`() {
|
||||
addMention("src/a.ts", "@ts")
|
||||
addMention("src/a.tsx", "@tsx")
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package ai.kilocode.client.session.ui.prompt
|
||||
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import java.nio.file.Path
|
||||
|
||||
class PromptMentionPartsTest : BasePlatformTestCase() {
|
||||
|
||||
fun `test mentionFileParts builds file part for tracked relative path`() {
|
||||
val parts = mentionFileParts("read @src/Main.kt", setOf("src/Main.kt"), "/repo")
|
||||
|
||||
assertEquals(1, parts.size)
|
||||
val part = parts.single()
|
||||
assertEquals("file", part.type)
|
||||
assertEquals("text/plain", part.mime)
|
||||
assertEquals(Path.of("/repo/src/Main.kt").toUri().toString(), part.url)
|
||||
assertEquals("Main.kt", part.filename)
|
||||
assertEquals("file", part.source?.type)
|
||||
assertEquals("src/Main.kt", part.source?.path)
|
||||
assertEquals("@src/Main.kt", part.source?.text?.value)
|
||||
assertEquals(5.0, part.source?.text?.start)
|
||||
assertEquals(17.0, part.source?.text?.end)
|
||||
}
|
||||
|
||||
fun `test mentionFileParts ignores untracked path`() {
|
||||
assertTrue(mentionFileParts("read @src/Main.kt", setOf("src/Other.kt"), "/repo").isEmpty())
|
||||
}
|
||||
|
||||
fun `test mentionFileParts keeps absolute paths absolute`() {
|
||||
val path = "/tmp/abs.txt"
|
||||
val part = mentionFileParts("read @$path", setOf(path), "/repo").single()
|
||||
|
||||
assertEquals(Path.of(path).toUri().toString(), part.url)
|
||||
assertEquals("abs.txt", part.filename)
|
||||
}
|
||||
|
||||
fun `test gitChangesPart builds encoded data part`() {
|
||||
val part = gitChangesPart("review @git-changes", "hello world+plus")!!
|
||||
|
||||
assertEquals("file", part.type)
|
||||
assertEquals("text/plain", part.mime)
|
||||
assertEquals("git-changes.txt", part.filename)
|
||||
assertEquals("data:text/plain;charset=utf-8,hello%20world%2Bplus", part.url)
|
||||
assertEquals("resource", part.source?.type)
|
||||
assertEquals("git-changes", part.source?.uri)
|
||||
assertEquals("jetbrains", part.source?.clientName)
|
||||
assertEquals("@git-changes", part.source?.text?.value)
|
||||
assertEquals(7.0, part.source?.text?.start)
|
||||
assertEquals(19.0, part.source?.text?.end)
|
||||
}
|
||||
|
||||
fun `test gitChangesPart ignores missing blank and non boundary matches`() {
|
||||
assertNull(gitChangesPart("review @git-changes", null))
|
||||
assertNull(gitChangesPart("review @git-changes", " "))
|
||||
assertNull(gitChangesPart("review @git-changes-foo", "diff"))
|
||||
}
|
||||
}
|
||||
+2
@@ -84,6 +84,7 @@ class FakeSessionRpcApi : KiloSessionRpcApi {
|
||||
var enhanced = "Enhanced prompt"
|
||||
var enhanceGate: CompletableDeferred<Unit>? = null
|
||||
var enhanceThrows: Exception? = null
|
||||
var commandThrows: Exception? = null
|
||||
val prompts = mutableListOf<Triple<String, String, PromptDto>>()
|
||||
val commands = mutableListOf<CommandCall>()
|
||||
val attachmentParts = mutableListOf<AttachmentCall>()
|
||||
@@ -199,6 +200,7 @@ class FakeSessionRpcApi : KiloSessionRpcApi {
|
||||
|
||||
override suspend fun command(id: String, directory: String, command: String, arguments: String, prompt: PromptDto) {
|
||||
assertNotEdt("command")
|
||||
commandThrows?.let { throw it }
|
||||
commands.add(CommandCall(id, directory, command, arguments, prompt))
|
||||
}
|
||||
|
||||
|
||||
-6
@@ -30,7 +30,6 @@ class FakeWorkspaceRpcApi : KiloWorkspaceRpcApi {
|
||||
var fileMatches = emptyList<WorkspaceFileDto>()
|
||||
var searchResult = FileSearchResultDto()
|
||||
var search: ((String) -> FileSearchResultDto)? = null
|
||||
var terminalOutput: String? = null
|
||||
var gitChanges: String? = null
|
||||
var openResult = true
|
||||
var localConfigPath = "/test/.kilo/kilo.jsonc"
|
||||
@@ -82,11 +81,6 @@ class FakeWorkspaceRpcApi : KiloWorkspaceRpcApi {
|
||||
return search?.invoke(query) ?: searchResult
|
||||
}
|
||||
|
||||
override suspend fun terminalOutput(directory: String): String? {
|
||||
assertNotEdt("terminalOutput")
|
||||
return terminalOutput
|
||||
}
|
||||
|
||||
override suspend fun gitChanges(directory: String): String? {
|
||||
assertNotEdt("gitChanges")
|
||||
return gitChanges
|
||||
|
||||
@@ -50,9 +50,6 @@ interface KiloWorkspaceRpcApi : RemoteApi<Unit> {
|
||||
/** Fuzzy file/folder search via the backend IDE index. */
|
||||
suspend fun searchFiles(directory: String, query: String, limit: Int = 50): FileSearchResultDto
|
||||
|
||||
/** Best-effort active terminal scrollback for @terminal mentions. */
|
||||
suspend fun terminalOutput(directory: String): String?
|
||||
|
||||
/** Current uncommitted git changes as a unified diff for @git-changes mentions. */
|
||||
suspend fun gitChanges(directory: String): String?
|
||||
|
||||
|
||||
@@ -13,6 +13,5 @@ data class WorkspaceFileDto(
|
||||
data class FileSearchResultDto(
|
||||
val indexing: Boolean = false,
|
||||
val files: List<WorkspaceFileDto> = emptyList(),
|
||||
val terminal: Boolean = false,
|
||||
val git: Boolean = false,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user