mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
feat(jetbrains): add Logging section with log reveal and backend log download
Group the Advanced settings log level and message-preview controls under a Logging section. Add a reveal action that opens the Kilo diagnostic log in the OS file manager (Finder/Explorer/file manager). In remote development, the client log is revealed locally and the remote backend log is fetched over RPC and saved through a client-side dialog.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": minor
|
||||
---
|
||||
|
||||
Add an Advanced settings page with a Logging section to configure diagnostic log level and message previews, reveal the log in your file manager, and download the backend log in remote development.
|
||||
+11
@@ -13,6 +13,7 @@ import ai.kilocode.backend.cli.KiloCliPlatform
|
||||
import ai.kilocode.backend.cli.KiloProps
|
||||
import ai.kilocode.backend.cli.KiloRepoCli
|
||||
import ai.kilocode.jetbrains.api.model.KiloProfile200Response
|
||||
import ai.kilocode.log.KiloLog
|
||||
import ai.kilocode.log.LogConfig
|
||||
import ai.kilocode.rpc.dto.ConfigPatchDto
|
||||
import ai.kilocode.rpc.KiloAppRpcApi
|
||||
@@ -24,6 +25,7 @@ import ai.kilocode.rpc.dto.KiloAppStatusDto
|
||||
import ai.kilocode.rpc.dto.LoadErrorDto
|
||||
import ai.kilocode.rpc.dto.LoadProgressDto
|
||||
import ai.kilocode.rpc.dto.LogConfigDto
|
||||
import ai.kilocode.rpc.dto.LogFileDto
|
||||
import ai.kilocode.rpc.dto.ModelFavoriteUpdateDto
|
||||
import ai.kilocode.rpc.dto.ModelSelectionUpdateDto
|
||||
import ai.kilocode.rpc.dto.ModelStateDto
|
||||
@@ -35,9 +37,12 @@ import ai.kilocode.rpc.dto.ProfileOrganizationDto
|
||||
import ai.kilocode.rpc.dto.ProfileStatusDto
|
||||
import ai.kilocode.rpc.dto.TelemetryCaptureDto
|
||||
import com.intellij.openapi.components.service
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.nio.file.Files
|
||||
|
||||
/**
|
||||
* Backend implementation of [KiloAppRpcApi].
|
||||
@@ -102,6 +107,12 @@ class KiloAppRpcApiImpl : KiloAppRpcApi {
|
||||
LogConfig.apply(config.level, config.contentMode, config.previewMax)
|
||||
}
|
||||
|
||||
override suspend fun backendLogFile(): LogFileDto? = withContext(Dispatchers.IO) {
|
||||
val path = KiloLog.logFile()
|
||||
if (!Files.exists(path)) return@withContext null
|
||||
LogFileDto(path.fileName.toString(), Files.readString(path))
|
||||
}
|
||||
|
||||
override suspend fun refreshProfile(): ProfileDto? = app.refreshProfile()?.let(::profileDto)
|
||||
|
||||
override suspend fun startLogin(directory: String?): DeviceAuthDto = app.startLogin(directory)
|
||||
|
||||
+9
@@ -9,6 +9,7 @@ import ai.kilocode.rpc.dto.HealthDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStateDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStatusDto
|
||||
import ai.kilocode.rpc.dto.LogConfigDto
|
||||
import ai.kilocode.rpc.dto.LogFileDto
|
||||
import ai.kilocode.rpc.dto.ModelFavoriteUpdateDto
|
||||
import ai.kilocode.rpc.dto.ModelSelectionDto
|
||||
import ai.kilocode.rpc.dto.ModelSelectionUpdateDto
|
||||
@@ -116,6 +117,14 @@ class KiloAppService internal constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/** Read the backend diagnostic log file for download in split mode. Null when absent or on failure. */
|
||||
suspend fun backendLog(): LogFileDto? = try {
|
||||
call { backendLogFile() }
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("backend log fetch failed", e)
|
||||
null
|
||||
}
|
||||
|
||||
/** One-shot health check. Returns null on failure. */
|
||||
suspend fun health(): HealthDto? = try {
|
||||
call { health() }
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package ai.kilocode.client.settings
|
||||
|
||||
import ai.kilocode.client.app.KiloAppService
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.log.KiloLog
|
||||
import ai.kilocode.rpc.dto.LogFileDto
|
||||
import com.intellij.ide.actions.RevealFileAction
|
||||
import com.intellij.notification.Notification
|
||||
import com.intellij.notification.NotificationGroupManager
|
||||
import com.intellij.notification.NotificationType
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.application.asContextElement
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.fileChooser.FileChooserFactory
|
||||
import com.intellij.openapi.fileChooser.FileSaverDescriptor
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.nio.file.Files
|
||||
import javax.swing.JComponent
|
||||
|
||||
/**
|
||||
* Diagnostic log actions for the Advanced settings page.
|
||||
*
|
||||
* In monolith mode the frontend and backend share one process, so a single reveal opens the log
|
||||
* in the OS file manager. In split mode the backend log lives on the remote host, so it is fetched
|
||||
* over RPC and saved through a client-side file dialog while the client log is revealed locally.
|
||||
*/
|
||||
internal object AdvancedLogActions {
|
||||
|
||||
/** OS-appropriate reveal label (e.g. "Reveal in Finder", "Show in Explorer"). */
|
||||
fun revealLabel(): String = RevealFileAction.getActionName()
|
||||
|
||||
fun reveal() {
|
||||
ApplicationManager.getApplication().executeOnPooledThread {
|
||||
val path = KiloLog.logFile()
|
||||
if (Files.exists(path)) RevealFileAction.openFile(path)
|
||||
else RevealFileAction.openDirectory(path.parent)
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadBackend(parent: JComponent) {
|
||||
val app = service<KiloAppService>()
|
||||
app.scope.launch {
|
||||
val log = app.backendLog()
|
||||
withContext(Dispatchers.EDT + ModalityState.any().asContextElement()) {
|
||||
save(parent, log)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun save(parent: JComponent, log: LogFileDto?) {
|
||||
if (log == null) {
|
||||
notify(NotificationType.WARNING, KiloBundle.message("settings.advanced.logs.backend.missing"))
|
||||
return
|
||||
}
|
||||
val descriptor = FileSaverDescriptor(
|
||||
KiloBundle.message("settings.advanced.logs.backend.save.title"),
|
||||
KiloBundle.message("settings.advanced.logs.backend.save.description"),
|
||||
"log",
|
||||
)
|
||||
val wrapper = FileChooserFactory.getInstance()
|
||||
.createSaveFileDialog(descriptor, parent)
|
||||
.save(null as VirtualFile?, log.name) ?: return
|
||||
ApplicationManager.getApplication().executeOnPooledThread {
|
||||
runCatching { wrapper.file.writeText(log.content, Charsets.UTF_8) }
|
||||
.onSuccess { notify(NotificationType.INFORMATION, KiloBundle.message("settings.advanced.logs.backend.saved", wrapper.file.name)) }
|
||||
.onFailure { notify(NotificationType.ERROR, KiloBundle.message("settings.advanced.logs.backend.failed"), it.message) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun notify(type: NotificationType, title: String, content: String? = null) {
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
val notification = NotificationGroupManager.getInstance()
|
||||
.getNotificationGroup("Kilo Code")
|
||||
?.createNotification(title, content.orEmpty(), type)
|
||||
?: Notification("Kilo Code", title, content.orEmpty(), type)
|
||||
notification.notify(ProjectManager.getInstance().openProjects.firstOrNull { !it.isDefault })
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -6,6 +6,9 @@ import ai.kilocode.client.settings.base.SettingsRows
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.log.LogConfig
|
||||
import com.intellij.openapi.ui.ComboBox
|
||||
import com.intellij.platform.ide.productMode.IdeProductMode
|
||||
import com.intellij.ui.TitledSeparator
|
||||
import com.intellij.ui.components.ActionLink
|
||||
import com.intellij.ui.components.JBTextField
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.BorderLayout
|
||||
@@ -30,6 +33,7 @@ internal class AdvancedSettingsUi : JPanel(BorderLayout()) {
|
||||
|
||||
val rows = SettingsRows().apply {
|
||||
border = JBUI.Borders.empty(UiStyle.Gap.pad(), UiStyle.Gap.lg())
|
||||
row(TitledSeparator(KiloBundle.message("settings.advanced.logging.title")))
|
||||
row(SettingsRow(
|
||||
KiloBundle.message("logs.configuration.level.title"),
|
||||
KiloBundle.message("logs.configuration.level.description"),
|
||||
@@ -45,6 +49,7 @@ internal class AdvancedSettingsUi : JPanel(BorderLayout()) {
|
||||
KiloBundle.message("logs.configuration.previewSize.description", LogConfig.MIN_PREVIEW, LogConfig.MAX_PREVIEW),
|
||||
preview,
|
||||
))
|
||||
logRows().forEach(::row)
|
||||
}
|
||||
add(rows, BorderLayout.CENTER)
|
||||
}
|
||||
@@ -83,4 +88,30 @@ internal class AdvancedSettingsUi : JPanel(BorderLayout()) {
|
||||
private fun current(): Values = Values(LogConfig.level(), LogConfig.contentMode(), LogConfig.previewMax())
|
||||
|
||||
private fun count(): Int? = preview.text.trim().toIntOrNull()
|
||||
|
||||
// In monolith mode one reveal opens the shared log; in split mode the client log is revealed
|
||||
// locally and the remote backend log is downloaded.
|
||||
private fun logRows(): List<SettingsRow> {
|
||||
if (IdeProductMode.isMonolith) {
|
||||
return listOf(SettingsRow(
|
||||
KiloBundle.message("settings.advanced.logs.title"),
|
||||
KiloBundle.message("settings.advanced.logs.description"),
|
||||
ActionLink(AdvancedLogActions.revealLabel()) { AdvancedLogActions.reveal() },
|
||||
))
|
||||
}
|
||||
return listOf(
|
||||
SettingsRow(
|
||||
KiloBundle.message("settings.advanced.logs.client.title"),
|
||||
KiloBundle.message("settings.advanced.logs.client.description"),
|
||||
ActionLink(AdvancedLogActions.revealLabel()) { AdvancedLogActions.reveal() },
|
||||
),
|
||||
SettingsRow(
|
||||
KiloBundle.message("settings.advanced.logs.backend.title"),
|
||||
KiloBundle.message("settings.advanced.logs.backend.description"),
|
||||
ActionLink(KiloBundle.message("settings.advanced.logs.backend.download")) {
|
||||
AdvancedLogActions.downloadBackend(this)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,6 +428,19 @@ settings.cli.unavailable.message=Settings are available after Kilo Code connects
|
||||
settings.models.displayName=Models
|
||||
settings.context.displayName=Context
|
||||
settings.advanced.displayName=Advanced
|
||||
settings.advanced.logging.title=Logging
|
||||
settings.advanced.logs.title=Diagnostic logs
|
||||
settings.advanced.logs.description=Open the Kilo diagnostic log in your file manager.
|
||||
settings.advanced.logs.client.title=Client log
|
||||
settings.advanced.logs.client.description=Open the JetBrains client diagnostic log in your file manager.
|
||||
settings.advanced.logs.backend.title=Backend log
|
||||
settings.advanced.logs.backend.description=Download the remote backend diagnostic log.
|
||||
settings.advanced.logs.backend.download=Download\u2026
|
||||
settings.advanced.logs.backend.missing=No backend diagnostic log is available yet.
|
||||
settings.advanced.logs.backend.save.title=Save Backend Log
|
||||
settings.advanced.logs.backend.save.description=Choose where to save the backend diagnostic log.
|
||||
settings.advanced.logs.backend.saved=Saved backend log to {0}
|
||||
settings.advanced.logs.backend.failed=Failed to save backend log
|
||||
settings.context.description=Configure context behavior.
|
||||
settings.context.save.pending=Saving context settings...
|
||||
settings.context.save.failed=Failed to save context settings
|
||||
|
||||
+15
@@ -5,6 +5,7 @@ import ai.kilocode.log.LogConfig
|
||||
import com.intellij.openapi.options.ConfigurationException
|
||||
import com.intellij.openapi.ui.ComboBox
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import com.intellij.ui.components.ActionLink
|
||||
import com.intellij.ui.components.JBTextField
|
||||
import java.awt.Container
|
||||
import javax.swing.JComponent
|
||||
@@ -91,8 +92,22 @@ class AdvancedConfigurableTest : BasePlatformTestCase() {
|
||||
}
|
||||
}
|
||||
|
||||
fun `test createComponent shows a reveal logs action`() {
|
||||
// Tests run in monolith mode, so a single OS-appropriate reveal link is shown.
|
||||
val cfg = configurable()
|
||||
edt {
|
||||
val root = cfg.createComponent()
|
||||
val labels = links(root as Container).map { it.text }
|
||||
assertTrue("expected a reveal-logs link, got $labels", labels.contains(AdvancedLogActions.revealLabel()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun configurable() = AdvancedConfigurable(settings) { it.applyLocal() }
|
||||
|
||||
private fun links(root: Container): List<ActionLink> = buildList {
|
||||
collect(root) { if (it is ActionLink) add(it) }
|
||||
}
|
||||
|
||||
private fun <T> edt(block: () -> T): T = edtWait(block)
|
||||
|
||||
private fun level(root: Container): ComboBox<*> = combos(root).single {
|
||||
|
||||
+16
@@ -3,6 +3,7 @@ package ai.kilocode.client.settings
|
||||
import ai.kilocode.client.app.KiloAppService
|
||||
import ai.kilocode.client.testing.FakeAppRpcApi
|
||||
import ai.kilocode.log.LogConfig
|
||||
import ai.kilocode.rpc.dto.LogFileDto
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -53,4 +54,19 @@ class KiloLogSettingsServiceTest : BasePlatformTestCase() {
|
||||
assertEquals("PREVIEW", dto.contentMode)
|
||||
assertEquals(25, dto.previewMax)
|
||||
}
|
||||
|
||||
fun `test backendLog returns the file from rpc`() = runBlocking(Dispatchers.Default) {
|
||||
rpc.backendLog = LogFileDto("kilo.log", "line one\nline two\n")
|
||||
|
||||
val log = app.backendLog()
|
||||
|
||||
assertEquals("kilo.log", log?.name)
|
||||
assertEquals("line one\nline two\n", log?.content)
|
||||
}
|
||||
|
||||
fun `test backendLog returns null when backend has no log`() = runBlocking(Dispatchers.Default) {
|
||||
rpc.backendLog = null
|
||||
|
||||
assertNull(app.backendLog())
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -10,6 +10,7 @@ import ai.kilocode.rpc.dto.HealthDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStateDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStatusDto
|
||||
import ai.kilocode.rpc.dto.LogConfigDto
|
||||
import ai.kilocode.rpc.dto.LogFileDto
|
||||
import ai.kilocode.rpc.dto.ModelFavoriteUpdateDto
|
||||
import ai.kilocode.rpc.dto.ModelSelectionDto
|
||||
import ai.kilocode.rpc.dto.ModelSelectionUpdateDto
|
||||
@@ -178,6 +179,13 @@ class FakeAppRpcApi : KiloAppRpcApi {
|
||||
logConfigs.add(config)
|
||||
}
|
||||
|
||||
var backendLog: LogFileDto? = null
|
||||
|
||||
override suspend fun backendLogFile(): LogFileDto? {
|
||||
assertNotEdt("backendLogFile")
|
||||
return backendLog
|
||||
}
|
||||
|
||||
private fun applyPatch(config: ConfigDto, patch: ConfigPatchDto): ConfigDto {
|
||||
val values = patch.values
|
||||
val agents = patch.agents.entries.fold(config.agent) { acc, (name, item) ->
|
||||
|
||||
@@ -67,6 +67,9 @@ interface KiloLog {
|
||||
|
||||
fun sandbox(): Boolean = System.getProperty("idea.plugin.in.sandbox.mode", "false").toBoolean()
|
||||
|
||||
/** Current diagnostic log file for this process (frontend or backend in split mode). */
|
||||
fun logFile(): Path = FileLog.logFile()
|
||||
|
||||
fun payload(log: KiloLog? = null): Map<String, String> = buildMap {
|
||||
put("platform", "jetbrains")
|
||||
put("client", "jetbrains")
|
||||
@@ -166,6 +169,8 @@ internal class FileLog(cls: Class<*>) : KiloLog {
|
||||
if (!initialized) return
|
||||
root.level = LogConfig.julLevel()
|
||||
}
|
||||
|
||||
internal fun logFile(): Path = resolveLogDir().resolve("kilo.log")
|
||||
}
|
||||
|
||||
override val isDebugEnabled: Boolean
|
||||
|
||||
@@ -5,6 +5,7 @@ import ai.kilocode.rpc.dto.ConfigPatchDto
|
||||
import ai.kilocode.rpc.dto.HealthDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStateDto
|
||||
import ai.kilocode.rpc.dto.LogConfigDto
|
||||
import ai.kilocode.rpc.dto.LogFileDto
|
||||
import ai.kilocode.rpc.dto.ModelFavoriteUpdateDto
|
||||
import ai.kilocode.rpc.dto.ModelSelectionUpdateDto
|
||||
import ai.kilocode.rpc.dto.ModelStateDto
|
||||
@@ -79,6 +80,9 @@ interface KiloAppRpcApi : RemoteApi<Unit> {
|
||||
/** Apply frontend-managed diagnostic log settings in the backend process. */
|
||||
suspend fun applyLogConfig(config: LogConfigDto)
|
||||
|
||||
/** Read the backend diagnostic log file for download in split mode. Null when absent. */
|
||||
suspend fun backendLogFile(): LogFileDto?
|
||||
|
||||
/** Refresh the user profile and return the latest data, or null if not logged in. */
|
||||
suspend fun refreshProfile(): ProfileDto?
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package ai.kilocode.rpc.dto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/** Backend diagnostic log file contents, transferred to the frontend for download in split mode. */
|
||||
@Serializable
|
||||
data class LogFileDto(
|
||||
val name: String,
|
||||
val content: String,
|
||||
)
|
||||
Reference in New Issue
Block a user