mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
fix(jetbrains): align reverted diff action
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show reverted-card diff actions inline with the session header and open rolled-back changes in the diff viewer.
|
||||
+58
-1
@@ -1516,14 +1516,71 @@ object KiloCliDataParser {
|
||||
private fun parseRevert(obj: JsonObject?): SessionRevertDto? {
|
||||
if (obj == null) return null
|
||||
val message = obj.str("messageID") ?: return null
|
||||
val diff = obj.str("diff")
|
||||
return SessionRevertDto(
|
||||
messageID = message,
|
||||
partID = obj.str("partID"),
|
||||
snapshot = obj.str("snapshot"),
|
||||
diff = obj.str("diff"),
|
||||
diff = diff,
|
||||
diffs = parseUnifiedDiff(diff),
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseUnifiedDiff(diff: String?): List<DiffFileDto> {
|
||||
if (diff.isNullOrBlank()) return emptyList()
|
||||
val lines = diff.lines()
|
||||
val starts = lines.mapIndexedNotNull { index, line -> if (line.startsWith("diff --git ")) index else null }
|
||||
if (starts.isEmpty()) return emptyList()
|
||||
return starts.mapIndexedNotNull { index, start ->
|
||||
val end = starts.getOrNull(index + 1) ?: lines.size
|
||||
parseUnifiedBlock(lines.subList(start, end).joinToString("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseUnifiedBlock(block: String): DiffFileDto? {
|
||||
val lines = block.lines()
|
||||
val file = unifiedFile(lines) ?: return null
|
||||
return DiffFileDto(
|
||||
file = file,
|
||||
additions = lines.count { it.startsWith("+") && !it.startsWith("+++") },
|
||||
deletions = lines.count { it.startsWith("-") && !it.startsWith("---") },
|
||||
patch = block,
|
||||
status = unifiedStatus(lines),
|
||||
)
|
||||
}
|
||||
|
||||
private fun unifiedFile(lines: List<String>): String? {
|
||||
val next = lines.firstOrNull { it.startsWith("+++ ") }?.removePrefix("+++ ")
|
||||
val prev = lines.firstOrNull { it.startsWith("--- ") }?.removePrefix("--- ")
|
||||
val path = sequenceOf(next, prev)
|
||||
.filterNotNull()
|
||||
.firstOrNull { it != "/dev/null" }
|
||||
?: lines.firstOrNull()?.let(::gitDiffTarget)
|
||||
return path?.let(::cleanDiffPath)
|
||||
}
|
||||
|
||||
private fun unifiedStatus(lines: List<String>): String = when {
|
||||
lines.any { it == "new file mode" || it.startsWith("new file mode ") } -> "added"
|
||||
lines.any { it == "deleted file mode" || it.startsWith("deleted file mode ") } -> "deleted"
|
||||
lines.any { it.startsWith("--- /dev/null") } -> "added"
|
||||
lines.any { it.startsWith("+++ /dev/null") } -> "deleted"
|
||||
else -> "modified"
|
||||
}
|
||||
|
||||
private fun gitDiffTarget(line: String): String? {
|
||||
val match = Regex("^diff --git a/(.*) b/(.*)$").find(line) ?: return null
|
||||
return match.groupValues.getOrNull(2)
|
||||
}
|
||||
|
||||
private fun cleanDiffPath(path: String): String {
|
||||
val text = path.trim().trim('"')
|
||||
return when {
|
||||
text.startsWith("a/") -> text.removePrefix("a/")
|
||||
text.startsWith("b/") -> text.removePrefix("b/")
|
||||
else -> text
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Internal — status parsing
|
||||
// ================================================================
|
||||
|
||||
+8
-1
@@ -691,7 +691,7 @@ class KiloCliDataParserTest {
|
||||
"messageID": "msg_rollback",
|
||||
"partID": "prt_rollback",
|
||||
"snapshot": "snap_rollback",
|
||||
"diff": "diff --git a/file b/file"
|
||||
"diff": "diff --git a/src/A.kt b/src/A.kt\n--- a/src/A.kt\n+++ b/src/A.kt\n@@ -1 +1,2 @@\n-old\n+new\n+more\ndiff --git a/src/Old.kt b/src/Old.kt\ndeleted file mode 100644\n--- a/src/Old.kt\n+++ /dev/null\n@@ -1 +0,0 @@\n-gone"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -705,6 +705,13 @@ class KiloCliDataParserTest {
|
||||
assertEquals(2, result.session.summary?.files)
|
||||
assertEquals("msg_rollback", result.session.revert?.messageID)
|
||||
assertEquals("prt_rollback", result.session.revert?.partID)
|
||||
assertEquals(2, result.session.revert?.diffs?.size)
|
||||
assertEquals("src/A.kt", result.session.revert?.diffs?.get(0)?.file)
|
||||
assertEquals(2, result.session.revert?.diffs?.get(0)?.additions)
|
||||
assertEquals(1, result.session.revert?.diffs?.get(0)?.deletions)
|
||||
assertEquals("modified", result.session.revert?.diffs?.get(0)?.status)
|
||||
assertEquals("src/Old.kt", result.session.revert?.diffs?.get(1)?.file)
|
||||
assertEquals("deleted", result.session.revert?.diffs?.get(1)?.status)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+116
-13
@@ -1,22 +1,32 @@
|
||||
package ai.kilocode.client.session.ui
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.SessionDiffOpener
|
||||
import ai.kilocode.client.session.model.SessionModel
|
||||
import ai.kilocode.client.session.model.SessionState
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
|
||||
import ai.kilocode.client.session.views.SessionViewIcons
|
||||
import ai.kilocode.client.session.views.base.BaseQuestionView
|
||||
import ai.kilocode.client.session.views.base.PartHeader
|
||||
import ai.kilocode.client.ui.DiffStatBadge
|
||||
import ai.kilocode.client.ui.ToolbarButtonAction
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.layout.Stack
|
||||
import ai.kilocode.client.ui.toolbarButton
|
||||
import ai.kilocode.rpc.dto.DiffFileDto
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.ui.JBFont
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Dimension
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.ScrollPaneConstants
|
||||
|
||||
class RevertBanner(
|
||||
private val model: SessionModel,
|
||||
@@ -24,15 +34,54 @@ class RevertBanner(
|
||||
private val redoAllAction: () -> Unit,
|
||||
private val cancelAction: () -> Unit,
|
||||
focus: (() -> Unit)? = null,
|
||||
private var openDiff: SessionDiffOpener = { _, _, _ -> },
|
||||
private var sessionId: String? = null,
|
||||
) : BorderLayoutPanel(), SessionView, SessionEditorStyleTarget {
|
||||
override val sessionViewKind = SessionView.Kind.Default
|
||||
|
||||
companion object {
|
||||
/** Cap the reverted-file list to this many rows before scrolling. */
|
||||
const val MAX_FILE_ROWS = 10
|
||||
}
|
||||
|
||||
private val card = BaseQuestionView(focus = focus)
|
||||
|
||||
private val title = JBLabel()
|
||||
|
||||
private val diff = toolbarButton(
|
||||
ToolbarButtonAction(SessionViewIcons.openDiff, KiloBundle.message("session.part.tool.openDiff"), ::openDiffViewer),
|
||||
).apply {
|
||||
isEnabled = false
|
||||
isVisible = false
|
||||
}
|
||||
|
||||
private val header = PartHeader().apply {
|
||||
leading(JBLabel(AllIcons.Actions.Back))
|
||||
left(title)
|
||||
left(PartHeader.centered(diff))
|
||||
}
|
||||
|
||||
private val body = Stack.vertical(UiStyle.Gap.lg())
|
||||
|
||||
private val files = Stack.vertical(UiStyle.Gap.xs())
|
||||
|
||||
private val scroll = object : JBScrollPane(
|
||||
files,
|
||||
ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
|
||||
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED,
|
||||
) {
|
||||
override fun getPreferredSize(): Dimension {
|
||||
val size = super.getPreferredSize()
|
||||
val cap = rowCap()
|
||||
return Dimension(size.width, if (cap > 0) minOf(size.height, cap) else size.height)
|
||||
}
|
||||
}.apply {
|
||||
border = JBUI.Borders.empty()
|
||||
viewportBorder = JBUI.Borders.empty()
|
||||
isOpaque = false
|
||||
viewport.isOpaque = false
|
||||
}
|
||||
|
||||
private val rows = LinkedHashMap<String, Row>()
|
||||
private var progress: RevertProgress? = null
|
||||
|
||||
@@ -46,8 +95,8 @@ class RevertBanner(
|
||||
|
||||
init {
|
||||
isOpaque = false
|
||||
card.setHeaderIcon(AllIcons.Actions.Back, KiloBundle.message("revert.message.rollback"))
|
||||
body.next(files).next(hint).next(notice)
|
||||
card.setTopPanel(header)
|
||||
body.next(scroll).next(hint).next(notice)
|
||||
card.setContent(body)
|
||||
card.setActions(listOf(
|
||||
BaseQuestionView.Action("redo", KiloBundle.message("revert.banner.redo"), primary = false) { redoAction() },
|
||||
@@ -58,22 +107,33 @@ class RevertBanner(
|
||||
update()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun setDiffOpener(openDiff: SessionDiffOpener, sessionId: String?) {
|
||||
this.openDiff = openDiff
|
||||
this.sessionId = sessionId
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun update() {
|
||||
val revert = model.revert()
|
||||
isVisible = revert != null
|
||||
if (revert == null) return
|
||||
val total = model.revertedCount()
|
||||
card.setHeader(KiloBundle.message(if (total == 1) "revert.banner.count.one" else "revert.banner.count.other", total))
|
||||
title.text = KiloBundle.message(if (total == 1) "revert.banner.count.one" else "revert.banner.count.other", total)
|
||||
card.setActionVisible("all", total > 1)
|
||||
notice.isVisible = revert.snapshot == null
|
||||
val keep = model.diff.mapTo(LinkedHashSet()) { it.file }
|
||||
val diffs = resolveDiffs(revert)
|
||||
val names = disambiguate(diffs.map { it.file })
|
||||
diff.isVisible = diffs.isNotEmpty()
|
||||
diff.isEnabled = diffs.isNotEmpty()
|
||||
val keep = diffs.mapTo(LinkedHashSet()) { it.file }
|
||||
rows.entries.removeIf { it.key !in keep }
|
||||
val order = model.diff.map { item ->
|
||||
scroll.isVisible = diffs.isNotEmpty()
|
||||
val order = diffs.map { item ->
|
||||
val row = rows.getOrPut(item.file) {
|
||||
Row(item.file)
|
||||
Row(item)
|
||||
}
|
||||
row.update(item.file, item.additions, item.deletions)
|
||||
row.update(item, names[item.file] ?: item.file)
|
||||
row.panel
|
||||
}
|
||||
if (files.components.toList() != order) {
|
||||
@@ -105,15 +165,42 @@ class RevertBanner(
|
||||
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
card.applyStyle(style)
|
||||
title.font = style.headerFont
|
||||
title.foreground = UiStyle.Colors.fg()
|
||||
progress?.applyStyle(style)
|
||||
hint.foreground = UIUtil.getLabelForeground()
|
||||
notice.foreground = UIUtil.getContextHelpForeground()
|
||||
rows.values.forEach { it.applyStyle() }
|
||||
}
|
||||
|
||||
private class Row(file: String) {
|
||||
private val label = JBLabel(file)
|
||||
private val badge = DiffStatBadge(0, 0)
|
||||
/**
|
||||
* The rolled-back diff to render. Prefer the snapshot diff the CLI attaches to the revert; fall
|
||||
* back to the session's current file diff when the pinned CLI doesn't provide it. Empty when no
|
||||
* snapshot exists (files were not restored, so there is nothing to diff).
|
||||
*/
|
||||
private fun resolveDiffs(revert: ai.kilocode.rpc.dto.SessionRevertDto): List<DiffFileDto> =
|
||||
if (revert.snapshot == null) emptyList() else revert.diffs.ifEmpty { model.diff }
|
||||
|
||||
private fun openDiffViewer() {
|
||||
val revert = model.revert() ?: return
|
||||
val diffs = resolveDiffs(revert)
|
||||
if (diffs.isEmpty()) return
|
||||
openDiff(diffs, KiloBundle.message("revert.banner.openDiff.title"), "revert:${sessionId ?: "pending"}:${revert.messageID}")
|
||||
}
|
||||
|
||||
/** Height that fits at most [MAX_FILE_ROWS] rows, or 0 when the list is short enough to show in full. */
|
||||
private fun rowCap(): Int {
|
||||
val comps = files.components
|
||||
if (comps.size <= MAX_FILE_ROWS) return 0
|
||||
val gap = UiStyle.Gap.xs()
|
||||
return (0 until MAX_FILE_ROWS).sumOf { comps[it].preferredSize.height } + gap * (MAX_FILE_ROWS - 1)
|
||||
}
|
||||
|
||||
private class Row(item: DiffFileDto) {
|
||||
private val label = JBLabel(item.file).apply {
|
||||
toolTipText = item.file
|
||||
}
|
||||
private val badge = DiffStatBadge(item.additions, item.deletions)
|
||||
val panel: JPanel = Stack.horizontal(UiStyle.Gap.sm())
|
||||
.next(label)
|
||||
.next(badge)
|
||||
@@ -122,9 +209,10 @@ class RevertBanner(
|
||||
applyStyle()
|
||||
}
|
||||
|
||||
fun update(file: String, additions: Int, deletions: Int) {
|
||||
if (label.text != file) label.text = file
|
||||
badge.update(additions, deletions)
|
||||
fun update(item: DiffFileDto, text: String) {
|
||||
if (label.text != text) label.text = text
|
||||
if (label.toolTipText != item.file) label.toolTipText = item.file
|
||||
badge.update(item.additions, item.deletions)
|
||||
}
|
||||
|
||||
fun applyStyle() {
|
||||
@@ -132,3 +220,18 @@ class RevertBanner(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun disambiguate(paths: List<String>): Map<String, String> {
|
||||
val parts = paths.associateWith { split(it) }
|
||||
return paths.groupBy { parts[it]?.lastOrNull().orEmpty() }.values.flatMap { group ->
|
||||
if (group.size == 1) return@flatMap listOf(group.first() to (parts[group.first()]?.lastOrNull() ?: group.first()))
|
||||
val depth = (1..(group.maxOf { parts[it]?.size ?: 1 })).firstOrNull { count ->
|
||||
group.map { suffix(parts[it].orEmpty(), count) }.toSet().size == group.size
|
||||
} ?: group.maxOf { parts[it]?.size ?: 1 }
|
||||
group.map { it to suffix(parts[it].orEmpty(), depth) }
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
private fun split(path: String): List<String> = path.split('/', '\\').filter { it.isNotEmpty() }
|
||||
|
||||
private fun suffix(parts: List<String>, depth: Int): String = parts.takeLast(depth).joinToString("/")
|
||||
|
||||
+1
@@ -214,6 +214,7 @@ class SessionMessageListPanel(
|
||||
fun setDiffOpener(openDiff: SessionDiffOpener, sessionId: String?) {
|
||||
this.openDiff = openDiff
|
||||
this.sessionId = sessionId
|
||||
banner?.setDiffOpener(openDiff, sessionId)
|
||||
turnViews.values.forEach { it.setDiffOpener(openDiff, sessionId) }
|
||||
}
|
||||
|
||||
|
||||
+11
-5
@@ -93,7 +93,9 @@ class BaseQuestionView(
|
||||
}
|
||||
|
||||
private val headerText: JBTextArea = makeText("", UiStyle.Colors.fg(), bold = true)
|
||||
private val descriptionText: JBTextArea = makeText("", UiStyle.Colors.weak(), bold = false)
|
||||
private val descriptionText: JBTextArea = makeText("", UiStyle.Colors.weak(), bold = false).apply {
|
||||
isVisible = false
|
||||
}
|
||||
|
||||
private var top: JComponent? = null
|
||||
private var content: JComponent? = null
|
||||
@@ -131,6 +133,7 @@ class BaseQuestionView(
|
||||
fun setHeader(text: String, description: String? = null) {
|
||||
headerText.text = text
|
||||
setDescription(description)
|
||||
syncNorth()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,6 +144,7 @@ class BaseQuestionView(
|
||||
fun setDescription(text: String?) {
|
||||
descriptionText.text = text ?: ""
|
||||
descriptionText.isVisible = !text.isNullOrBlank()
|
||||
syncNorth()
|
||||
}
|
||||
|
||||
// ---- public slot API ----
|
||||
@@ -170,8 +174,7 @@ class BaseQuestionView(
|
||||
if (icon == null && attached) header.remove(this.icon)
|
||||
this.icon.revalidate()
|
||||
this.icon.repaint()
|
||||
header.revalidate()
|
||||
header.repaint()
|
||||
syncNorth()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -297,12 +300,14 @@ class BaseQuestionView(
|
||||
private fun syncNorth() {
|
||||
north.removeAll()
|
||||
top?.let { north.next(it) }
|
||||
north.next(header)
|
||||
if (hasHeader()) north.next(header)
|
||||
if (content != null) north.fill(gap)
|
||||
north.revalidate()
|
||||
north.repaint()
|
||||
}
|
||||
|
||||
private fun hasHeader() = icon.icon != null || headerText.text.isNotBlank() || descriptionText.isVisible
|
||||
|
||||
private fun syncFooter() {
|
||||
val layout = footer.layout as BorderLayout
|
||||
val west = layout.getLayoutComponent(BorderLayout.WEST)
|
||||
@@ -331,7 +336,8 @@ class BaseQuestionView(
|
||||
|
||||
private fun makeText(value: String, color: Color, bold: Boolean): JBTextArea {
|
||||
val area = object : JBTextArea(value) {
|
||||
override fun getPreferredSize() = withWidth(super.getPreferredSize().height)
|
||||
override fun getPreferredSize() =
|
||||
withWidth(super.getPreferredSize().height)
|
||||
|
||||
override fun getMaximumSize(): Dimension {
|
||||
val size = preferredSize
|
||||
|
||||
@@ -44,6 +44,7 @@ revert.banner.redo=Redo
|
||||
revert.banner.redo.all=Redo All
|
||||
revert.banner.hint=You can redo these changes until you send a new message
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.openDiff.title=Rolled back changes
|
||||
revert.message.rollback=Rollback to this message
|
||||
session.status.rollingback=Rolling back\u2026
|
||||
session.status.redoing=Redoing\u2026
|
||||
|
||||
@@ -427,6 +427,7 @@ settings.agentBehavior.skills.sources.editPath.title=تعديل مسار الم
|
||||
settings.agentBehavior.skills.sources.editUrl.title=تعديل URL المهارات
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.openDiff.title=Rolled back changes
|
||||
revert.banner.count.one=تم التراجع عن رسالة واحدة
|
||||
revert.banner.count.other=تم التراجع عن {0} رسائل
|
||||
revert.banner.redo=إعادة
|
||||
|
||||
@@ -427,6 +427,7 @@ settings.agentBehavior.skills.sources.editPath.title=Uredi putanju vještina
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Uredi URL vještina
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.openDiff.title=Rolled back changes
|
||||
revert.banner.count.one={0} poruka vraćena
|
||||
revert.banner.count.other={0} poruka vraćeno
|
||||
revert.banner.redo=Ponovi
|
||||
|
||||
@@ -427,6 +427,7 @@ settings.agentBehavior.skills.sources.editPath.title=Rediger færdighedssti
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Rediger færdigheds-URL
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.openDiff.title=Rolled back changes
|
||||
revert.banner.count.one={0} besked rullet tilbage
|
||||
revert.banner.count.other={0} beskeder rullet tilbage
|
||||
revert.banner.redo=Gentag
|
||||
|
||||
@@ -427,6 +427,7 @@ settings.agentBehavior.skills.sources.editPath.title=Skill-Pfad bearbeiten
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Skill-URL bearbeiten
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.openDiff.title=Rolled back changes
|
||||
revert.banner.count.one={0} Nachricht zurückgesetzt
|
||||
revert.banner.count.other={0} Nachrichten zurückgesetzt
|
||||
revert.banner.redo=Wiederholen
|
||||
|
||||
@@ -427,6 +427,7 @@ settings.agentBehavior.skills.sources.editPath.title=Editar ruta de habilidades
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Editar URL de habilidades
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.openDiff.title=Rolled back changes
|
||||
revert.banner.count.one={0} mensaje revertido
|
||||
revert.banner.count.other={0} mensajes revertidos
|
||||
revert.banner.redo=Rehacer
|
||||
|
||||
@@ -427,6 +427,7 @@ settings.agentBehavior.skills.sources.editPath.title=Modifier le chemin des comp
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Modifier l’URL des compétences
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.openDiff.title=Rolled back changes
|
||||
revert.banner.count.one={0} message annulé
|
||||
revert.banner.count.other={0} messages annulés
|
||||
revert.banner.redo=Rétablir
|
||||
|
||||
@@ -427,6 +427,7 @@ settings.agentBehavior.skills.sources.editPath.title=スキルパスを編集
|
||||
settings.agentBehavior.skills.sources.editUrl.title=スキル URL を編集
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.openDiff.title=Rolled back changes
|
||||
revert.banner.count.one={0} 件のメッセージをロールバックしました
|
||||
revert.banner.count.other={0} 件のメッセージをロールバックしました
|
||||
revert.banner.redo=やり直し
|
||||
|
||||
@@ -427,6 +427,7 @@ settings.agentBehavior.skills.sources.editPath.title=스킬 경로 편집
|
||||
settings.agentBehavior.skills.sources.editUrl.title=스킬 URL 편집
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.openDiff.title=Rolled back changes
|
||||
revert.banner.count.one={0}개 메시지가 롤백됨
|
||||
revert.banner.count.other={0}개 메시지가 롤백됨
|
||||
revert.banner.redo=다시 실행
|
||||
|
||||
@@ -427,6 +427,7 @@ settings.agentBehavior.skills.sources.editPath.title=Vaardigheidspad bewerken
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Vaardigheids-URL bewerken
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.openDiff.title=Rolled back changes
|
||||
revert.banner.count.one={0} bericht teruggedraaid
|
||||
revert.banner.count.other={0} berichten teruggedraaid
|
||||
revert.banner.redo=Opnieuw uitvoeren
|
||||
|
||||
@@ -427,6 +427,7 @@ settings.agentBehavior.skills.sources.editPath.title=Rediger ferdighetssti
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Rediger ferdighets-URL
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.openDiff.title=Rolled back changes
|
||||
revert.banner.count.one={0} melding rullet tilbake
|
||||
revert.banner.count.other={0} meldinger rullet tilbake
|
||||
revert.banner.redo=Gjør om
|
||||
|
||||
@@ -427,6 +427,7 @@ settings.agentBehavior.skills.sources.editPath.title=Edytuj ścieżkę umiejętn
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Edytuj URL umiejętności
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.openDiff.title=Rolled back changes
|
||||
revert.banner.count.one=Cofnięto {0} wiadomość
|
||||
revert.banner.count.other=Cofnięto {0} wiadomości
|
||||
revert.banner.redo=Ponów
|
||||
|
||||
+1
@@ -427,6 +427,7 @@ settings.agentBehavior.skills.sources.editPath.title=Editar caminho de habilidad
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Editar URL de habilidades
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.openDiff.title=Rolled back changes
|
||||
revert.banner.count.one={0} mensagem revertida
|
||||
revert.banner.count.other={0} mensagens revertidas
|
||||
revert.banner.redo=Refazer
|
||||
|
||||
@@ -427,6 +427,7 @@ settings.agentBehavior.skills.sources.editPath.title=Изменить путь
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Изменить URL навыков
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.openDiff.title=Rolled back changes
|
||||
revert.banner.count.one=Отменено сообщений: {0}
|
||||
revert.banner.count.other=Отменено сообщений: {0}
|
||||
revert.banner.redo=Повторить
|
||||
|
||||
@@ -427,6 +427,7 @@ settings.agentBehavior.skills.sources.editPath.title=แก้ไขเส้น
|
||||
settings.agentBehavior.skills.sources.editUrl.title=แก้ไข URL ทักษะ
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.openDiff.title=Rolled back changes
|
||||
revert.banner.count.one=ย้อนกลับข้อความ {0} รายการแล้ว
|
||||
revert.banner.count.other=ย้อนกลับข้อความ {0} รายการแล้ว
|
||||
revert.banner.redo=ทำซ้ำ
|
||||
|
||||
@@ -427,6 +427,7 @@ settings.agentBehavior.skills.sources.editPath.title=Beceri Yolunu Düzenle
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Beceri URL'sini Düzenle
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.openDiff.title=Rolled back changes
|
||||
revert.banner.count.one={0} mesaj geri alındı
|
||||
revert.banner.count.other={0} mesaj geri alındı
|
||||
revert.banner.redo=Yinele
|
||||
|
||||
@@ -427,6 +427,7 @@ settings.agentBehavior.skills.sources.editPath.title=Редагувати шля
|
||||
settings.agentBehavior.skills.sources.editUrl.title=Редагувати URL навичок
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.openDiff.title=Rolled back changes
|
||||
revert.banner.count.one=Відкочено повідомлень: {0}
|
||||
revert.banner.count.other=Відкочено повідомлень: {0}
|
||||
revert.banner.redo=Повторити
|
||||
|
||||
+1
@@ -427,6 +427,7 @@ settings.agentBehavior.skills.sources.editPath.title=编辑技能路径
|
||||
settings.agentBehavior.skills.sources.editUrl.title=编辑技能 URL
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.openDiff.title=Rolled back changes
|
||||
revert.banner.count.one=已回滚 {0} 条消息
|
||||
revert.banner.count.other=已回滚 {0} 条消息
|
||||
revert.banner.redo=重做
|
||||
|
||||
+1
@@ -427,6 +427,7 @@ settings.agentBehavior.skills.sources.editPath.title=編輯技能路徑
|
||||
settings.agentBehavior.skills.sources.editUrl.title=編輯技能 URL
|
||||
session.file.missing=Couldn''t find ''{0}'' in this repository.
|
||||
revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed.
|
||||
revert.banner.openDiff.title=Rolled back changes
|
||||
revert.banner.count.one=已回復 {0} 則訊息
|
||||
revert.banner.count.other=已回復 {0} 則訊息
|
||||
revert.banner.redo=重做
|
||||
|
||||
+108
-3
@@ -15,6 +15,7 @@ import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.LoginRequiredView
|
||||
import ai.kilocode.client.session.views.PlanExitView
|
||||
import ai.kilocode.client.session.views.base.BaseQuestionView
|
||||
import ai.kilocode.client.session.views.base.PartHeader
|
||||
import ai.kilocode.client.session.views.permission.PermissionView
|
||||
import ai.kilocode.client.session.views.question.QuestionResultView
|
||||
import ai.kilocode.client.session.views.question.QuestionView
|
||||
@@ -29,6 +30,7 @@ import ai.kilocode.client.session.views.tool.ToolView
|
||||
import ai.kilocode.client.session.views.todo.TodoWriteView
|
||||
import ai.kilocode.client.ui.DiffStatBadge
|
||||
import ai.kilocode.client.ui.HoverIcon
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.layout.Stack
|
||||
import ai.kilocode.rpc.dto.DiffFileDto
|
||||
import ai.kilocode.rpc.dto.MessageDto
|
||||
@@ -60,6 +62,7 @@ import javax.swing.JButton
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.RepaintManager
|
||||
import javax.swing.ScrollPaneConstants
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.border.Border
|
||||
|
||||
@@ -1080,8 +1083,9 @@ class SessionMessageListPanelTest : BasePlatformTestCase() {
|
||||
banner.update()
|
||||
|
||||
assertNotNull(find<BaseQuestionView>(banner))
|
||||
assertNotNull(components(banner).filterIsInstance<PartHeader>().singleOrNull())
|
||||
|
||||
val buttons = components(banner).filterIsInstance<JButton>()
|
||||
val buttons = components(banner).filterIsInstance<JButton>().filter { it.text.isNotEmpty() }
|
||||
assertEquals(
|
||||
listOf(KiloBundle.message("revert.banner.redo"), KiloBundle.message("revert.banner.redo.all")),
|
||||
buttons.map { it.text },
|
||||
@@ -1098,7 +1102,7 @@ class SessionMessageListPanelTest : BasePlatformTestCase() {
|
||||
fun `test rollback banner reuses file rows across updates`() {
|
||||
val banner = RevertBanner(model, {}, {}, {})
|
||||
model.upsertMessage(msg("u1", "user"))
|
||||
model.setRevert(SessionRevertDto("u1"))
|
||||
model.setRevert(SessionRevertDto("u1", snapshot = "snap1"))
|
||||
model.setDiff(listOf(DiffFileDto("src/A.kt", 1, 0), DiffFileDto("src/B.kt", 2, 1)))
|
||||
banner.update()
|
||||
val rows = components(banner).filterIsInstance<Stack>().filter { stack ->
|
||||
@@ -1122,6 +1126,102 @@ class SessionMessageListPanelTest : BasePlatformTestCase() {
|
||||
assertEquals("-2", badges[1].removedLabelForTest().text)
|
||||
}
|
||||
|
||||
fun `test rollback banner caps file list with scroll pane`() {
|
||||
val banner = RevertBanner(model, {}, {}, {})
|
||||
model.upsertMessage(msg("u1", "user"))
|
||||
model.setRevert(SessionRevertDto("u1", snapshot = "snap1"))
|
||||
model.setDiff((1..80).map { DiffFileDto("src/file-$it.kt", it, 0) })
|
||||
|
||||
banner.update()
|
||||
|
||||
val scroll = components(banner).filterIsInstance<JBScrollPane>().single()
|
||||
assertTrue(scroll.verticalScrollBarPolicy == ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED)
|
||||
assertTrue(scroll.horizontalScrollBarPolicy == ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED)
|
||||
val rows = rowLabels(banner).mapNotNull { it.parent }
|
||||
val rowHeight = rows.first().preferredSize.height
|
||||
val cap = rowHeight * RevertBanner.MAX_FILE_ROWS + UiStyle.Gap.xs() * (RevertBanner.MAX_FILE_ROWS - 1)
|
||||
assertEquals(cap, scroll.preferredSize.height)
|
||||
}
|
||||
|
||||
fun `test rollback banner shortens duplicate file names with parents`() {
|
||||
val banner = RevertBanner(model, {}, {}, {})
|
||||
model.upsertMessage(msg("u1", "user"))
|
||||
model.setRevert(SessionRevertDto("u1", snapshot = "snap1"))
|
||||
model.setDiff(listOf(
|
||||
DiffFileDto("apps/main/src/App.kt", 1, 0),
|
||||
DiffFileDto("packages/ui/src/App.kt", 2, 1),
|
||||
DiffFileDto("packages/ui/src/Button.kt", 3, 0),
|
||||
))
|
||||
|
||||
banner.update()
|
||||
|
||||
val labels = rowLabels(banner).map { it.text to it.toolTipText }
|
||||
assertTrue(labels.contains("main/src/App.kt" to "apps/main/src/App.kt"))
|
||||
assertTrue(labels.contains("ui/src/App.kt" to "packages/ui/src/App.kt"))
|
||||
assertTrue(labels.contains("Button.kt" to "packages/ui/src/Button.kt"))
|
||||
}
|
||||
|
||||
fun `test rollback banner opens rolled back diff`() {
|
||||
val diff = DiffFileDto("src/A.kt", 1, 0, PATCH, "modified")
|
||||
val opened = mutableListOf<List<DiffFileDto>>()
|
||||
val titles = mutableListOf<String>()
|
||||
val keys = mutableListOf<String>()
|
||||
val banner = RevertBanner(model, {}, {}, {})
|
||||
banner.setDiffOpener({ files, title, key ->
|
||||
opened.add(files)
|
||||
titles.add(title)
|
||||
keys.add(key)
|
||||
}, "ses_1")
|
||||
model.upsertMessage(msg("u1", "user"))
|
||||
model.setRevert(SessionRevertDto("u1", snapshot = "snap1", diffs = listOf(diff)))
|
||||
|
||||
banner.update()
|
||||
|
||||
val button = components(banner).filterIsInstance<HoverIcon>()
|
||||
.first { it.toolTipText == KiloBundle.message("session.part.tool.openDiff") }
|
||||
assertTrue(button.isVisible)
|
||||
assertTrue(button.isEnabled)
|
||||
button.doClick()
|
||||
|
||||
assertEquals(listOf(diff), opened.single())
|
||||
assertEquals(KiloBundle.message("revert.banner.openDiff.title"), titles.single())
|
||||
assertEquals("revert:ses_1:u1", keys.single())
|
||||
}
|
||||
|
||||
fun `test rollback banner hides open diff without a snapshot`() {
|
||||
val banner = RevertBanner(model, {}, {}, {})
|
||||
model.upsertMessage(msg("u1", "user"))
|
||||
model.setRevert(SessionRevertDto("u1", snapshot = null))
|
||||
model.setDiff(listOf(DiffFileDto("src/A.kt", 1, 0, PATCH)))
|
||||
|
||||
banner.update()
|
||||
|
||||
val button = components(banner).filterIsInstance<HoverIcon>()
|
||||
.first { it.toolTipText == KiloBundle.message("session.part.tool.openDiff") }
|
||||
assertFalse(button.isVisible)
|
||||
assertFalse(button.isEnabled)
|
||||
}
|
||||
|
||||
fun `test rollback banner opens session diff when revert diff is absent`() {
|
||||
val diff = DiffFileDto("src/A.kt", 1, 0, PATCH, "modified")
|
||||
val opened = mutableListOf<List<DiffFileDto>>()
|
||||
val banner = RevertBanner(model, {}, {}, {})
|
||||
banner.setDiffOpener({ files, _, _ -> opened.add(files) }, "ses_1")
|
||||
model.upsertMessage(msg("u1", "user"))
|
||||
model.setRevert(SessionRevertDto("u1", snapshot = "snap1"))
|
||||
model.setDiff(listOf(diff))
|
||||
|
||||
banner.update()
|
||||
|
||||
val button = components(banner).filterIsInstance<HoverIcon>()
|
||||
.first { it.toolTipText == KiloBundle.message("session.part.tool.openDiff") }
|
||||
assertTrue(button.isVisible)
|
||||
assertTrue(button.isEnabled)
|
||||
button.doClick()
|
||||
|
||||
assertEquals(listOf(diff), opened.single())
|
||||
}
|
||||
|
||||
fun `test rollback banner shows redo all only for multiple reverted messages`() {
|
||||
val banner = RevertBanner(model, {}, {}, {})
|
||||
model.upsertMessage(msg("u1", "user"))
|
||||
@@ -1168,7 +1268,7 @@ class SessionMessageListPanelTest : BasePlatformTestCase() {
|
||||
|
||||
banner.setReverting(SessionState.Reverting("Rolling back...", SessionState.Reverting.Kind.ROLLBACK, "u1"))
|
||||
|
||||
val buttons = components(banner).filterIsInstance<JButton>()
|
||||
val buttons = components(banner).filterIsInstance<JButton>().filter { it.text.isNotEmpty() }
|
||||
assertTrue(buttons.filter { it.text == KiloBundle.message("revert.banner.redo") }.all { !it.isEnabled })
|
||||
assertTrue(buttons.filter { it.text == KiloBundle.message("revert.banner.redo.all") }.all { !it.isEnabled })
|
||||
val progress = components(banner).filterIsInstance<RevertProgress>().single()
|
||||
@@ -1521,6 +1621,11 @@ class SessionMessageListPanelTest : BasePlatformTestCase() {
|
||||
return out
|
||||
}
|
||||
|
||||
private fun rowLabels(root: Component): List<JBLabel> = components(root)
|
||||
.filterIsInstance<Stack>()
|
||||
.filter { stack -> stack.components.any { it is DiffStatBadge } }
|
||||
.mapNotNull { stack -> components(stack).filterIsInstance<JBLabel>().firstOrNull() }
|
||||
|
||||
private fun taskText(view: TaskToolView): List<String> {
|
||||
val scroll = components(view).filterIsInstance<JBScrollPane>().single()
|
||||
val stack = components(scroll.viewport.view).filterIsInstance<Stack>().single()
|
||||
|
||||
+5
-4
@@ -21,12 +21,11 @@ class BaseQuestionViewTest : BasePlatformTestCase() {
|
||||
|
||||
// ------ initial state ------
|
||||
|
||||
fun `test header and description text areas are in the component tree by default`() {
|
||||
fun `test empty card does not render a header row by default`() {
|
||||
edt {
|
||||
val panel = BaseQuestionView()
|
||||
assertTrue("Root layout should be BorderLayout", panel.layout is BorderLayout)
|
||||
val areas = findAll<JBTextArea>(panel)
|
||||
assertTrue("Should have at least 2 text areas (header + description)", areas.size >= 2)
|
||||
assertNull("Header row should be omitted until header content exists", headerRow(panel))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +86,7 @@ class BaseQuestionViewTest : BasePlatformTestCase() {
|
||||
fun `test setTopPanel adds component before header`() {
|
||||
edt {
|
||||
val panel = BaseQuestionView()
|
||||
panel.setHeader("Title")
|
||||
val top = JLabel("top")
|
||||
panel.setTopPanel(top)
|
||||
|
||||
@@ -280,9 +280,10 @@ class BaseQuestionViewTest : BasePlatformTestCase() {
|
||||
}
|
||||
}
|
||||
|
||||
fun `test header row has no west icon gap by default`() {
|
||||
fun `test header row has no west icon gap without icon`() {
|
||||
edt {
|
||||
val panel = BaseQuestionView()
|
||||
panel.setHeader("Title")
|
||||
val header = headerRow(panel)!!
|
||||
val west = (header.layout as BorderLayout).getLayoutComponent(BorderLayout.WEST)
|
||||
assertNull("header should not reserve icon space when icon is absent", west)
|
||||
|
||||
@@ -21,6 +21,7 @@ data class SessionRevertDto(
|
||||
val partID: String? = null,
|
||||
val snapshot: String? = null,
|
||||
val diff: String? = null,
|
||||
val diffs: List<DiffFileDto> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
||||
Reference in New Issue
Block a user