mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
feat(jetbrains): add cloud history repo-only filter
Resolve remaining parity gap from PR #10097 review: VS Code's cloud history has an 'Only this repository' checkbox that filters sessions by git remote URL; JetBrains had the RPC plumbing but no UI or URL lookup. - Add GitRemoteUrl.kt helper that resolves 'origin' via git subprocess - HistoryController gains gitUrl (resolved once on first cloud load) and repoOnly (defaults true when a URL exists); applyRepoOnly() toggles the filter and reloads cloud history - HistoryPanel adds a JBCheckBox on the cloud tab, visible only when a git remote URL is found, with left indent aligned to list item text - Add bundle key history.cloud.repo.only - Extend HistoryControllerTest with 8 new cases covering URL filtering, null fallback, load-more retention, and checkbox panel behaviour
This commit is contained in:
+19
@@ -0,0 +1,19 @@
|
||||
package ai.kilocode.client.session.history
|
||||
|
||||
/**
|
||||
* Resolves the origin remote URL for a given directory by running
|
||||
* `git remote get-url origin` as a subprocess.
|
||||
*
|
||||
* Returns null when the directory is not a git repo, has no origin remote,
|
||||
* or the command fails for any reason.
|
||||
*
|
||||
* Overridable in tests via [resolve] parameter.
|
||||
*/
|
||||
internal fun resolveGitRemoteUrl(dir: String): String? = runCatching {
|
||||
val proc = ProcessBuilder("git", "remote", "get-url", "origin")
|
||||
.directory(java.io.File(dir))
|
||||
.start()
|
||||
val out = proc.inputStream.bufferedReader().readText().trim()
|
||||
val code = proc.waitFor()
|
||||
if (code == 0 && out.isNotEmpty()) out else null
|
||||
}.getOrNull()
|
||||
+45
-1
@@ -16,6 +16,7 @@ class HistoryController(
|
||||
private val cs: CoroutineScope,
|
||||
open: (SessionRef) -> Unit = {},
|
||||
private val deleted: (String) -> Unit = {},
|
||||
private val gitUrlProvider: () -> String? = { resolveGitRemoteUrl(workspace.directory) },
|
||||
) {
|
||||
companion object {
|
||||
const val CLOUD_LIMIT = 50
|
||||
@@ -24,6 +25,24 @@ class HistoryController(
|
||||
val local = HistoryModel<LocalHistoryItem>()
|
||||
val cloud = CloudHistoryModel()
|
||||
|
||||
/** Resolved once on first cloud load; null means no remote found. Written from IO, read on EDT. */
|
||||
@Volatile
|
||||
var gitUrl: String? = null
|
||||
private set
|
||||
|
||||
/** Whether to filter cloud history by the current repository. */
|
||||
var repoOnly: Boolean = false
|
||||
private set
|
||||
|
||||
/** Notified on EDT when [repoOnly] changes (e.g. to update checkbox state). */
|
||||
var onRepoOnlyChanged: ((Boolean) -> Unit)? = null
|
||||
|
||||
private fun updateRepoOnly(value: Boolean) {
|
||||
if (repoOnly == value) return
|
||||
repoOnly = value
|
||||
edt { onRepoOnlyChanged?.invoke(value) }
|
||||
}
|
||||
|
||||
private val deleting = mutableSetOf<String>()
|
||||
private val opener = open
|
||||
|
||||
@@ -54,6 +73,11 @@ class HistoryController(
|
||||
loadCloud(reset = false)
|
||||
}
|
||||
|
||||
fun applyRepoOnly(value: Boolean) {
|
||||
updateRepoOnly(value)
|
||||
edt { reloadCloud() }
|
||||
}
|
||||
|
||||
fun delete(item: LocalHistoryItem) {
|
||||
edt {
|
||||
if (item.id in deleting) return@edt
|
||||
@@ -104,8 +128,10 @@ class HistoryController(
|
||||
val cursor = cloud.cursor.takeUnless { reset }
|
||||
edt { cloud.start(reset) }
|
||||
cs.launch {
|
||||
val url = resolveUrlIfNeeded()
|
||||
val filter = if (repoOnly) url else null
|
||||
try {
|
||||
val result = sessions.cloudSessions(workspace.directory, cursor, CLOUD_LIMIT, null)
|
||||
val result = sessions.cloudSessions(workspace.directory, cursor, CLOUD_LIMIT, filter)
|
||||
val items = result.sessions.map(::cloudItem)
|
||||
edt {
|
||||
if (reset) cloud.replace(items, result.nextCursor)
|
||||
@@ -116,6 +142,24 @@ class HistoryController(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves [gitUrl] on first cloud load. Subsequent calls return the cached value.
|
||||
* Also enables [repoOnly] by default when a URL is found the first time.
|
||||
*
|
||||
* Must be called from a coroutine. Writes [gitUrl] directly (volatile) and then
|
||||
* propagates state updates to EDT via [edt].
|
||||
*/
|
||||
private suspend fun resolveUrlIfNeeded(): String? {
|
||||
if (gitUrl != null) return gitUrl
|
||||
val url = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
|
||||
gitUrlProvider()
|
||||
}
|
||||
// Write gitUrl directly (volatile) so it is visible before EDT callbacks fire.
|
||||
gitUrl = url
|
||||
if (url != null) updateRepoOnly(true)
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
private fun edt(block: () -> Unit) {
|
||||
|
||||
+26
-2
@@ -22,6 +22,7 @@ import com.intellij.ui.DocumentAdapter
|
||||
import com.intellij.ui.PopupHandler
|
||||
import com.intellij.ui.SearchTextField
|
||||
import com.intellij.ui.ScrollingUtil
|
||||
import com.intellij.ui.components.JBCheckBox
|
||||
import com.intellij.ui.components.JBList
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.ui.tabs.JBTabs
|
||||
@@ -64,6 +65,11 @@ class HistoryPanel(
|
||||
private val localList = localList()
|
||||
private val cloudList = cloudList()
|
||||
private val more = LoadMoreButton()
|
||||
private val repoOnly = JBCheckBox(KiloBundle.message("history.cloud.repo.only"), true).apply {
|
||||
isVisible = false
|
||||
border = JBUI.Borders.emptyLeft(UiStyle.Gap.lg())
|
||||
addActionListener { controller.applyRepoOnly(isSelected) }
|
||||
}
|
||||
private val localPanel = panel(localSearch, localList)
|
||||
private val cloudPanel = panel(cloudSearch, cloudList, more)
|
||||
private val cards = CardLayout()
|
||||
@@ -96,6 +102,9 @@ class HistoryPanel(
|
||||
bind(localList, controller.local)
|
||||
bind(cloudList, controller.cloud)
|
||||
bindTheme()
|
||||
controller.onRepoOnlyChanged = { value ->
|
||||
repoOnly.isSelected = value
|
||||
}
|
||||
addHierarchyListener { e ->
|
||||
if (e.changeFlags and HierarchyEvent.SHOWING_CHANGED.toLong() == 0L) return@addHierarchyListener
|
||||
if (isShowing && stale) {
|
||||
@@ -186,7 +195,13 @@ class HistoryPanel(
|
||||
|
||||
private fun panel(search: SearchTextField, list: JList<out HistoryItem>, footer: JComponent? = null): JComponent {
|
||||
return BorderLayoutPanel().apply {
|
||||
add(search, BorderLayout.NORTH)
|
||||
val north = BorderLayoutPanel().apply {
|
||||
add(search, BorderLayout.CENTER)
|
||||
if (list === cloudList) {
|
||||
add(repoOnly, BorderLayout.SOUTH)
|
||||
}
|
||||
}
|
||||
add(north, BorderLayout.NORTH)
|
||||
add(JBScrollPane(list).apply {
|
||||
border = JBUI.Borders.empty()
|
||||
viewportBorder = JBUI.Borders.empty()
|
||||
@@ -265,6 +280,7 @@ class HistoryPanel(
|
||||
syncList(cloudList, controller.cloud)
|
||||
more.isEnabled = controller.cloud.cursor != null && !controller.cloud.loading
|
||||
more.isVisible = controller.cloud.cursor != null || controller.cloud.loading
|
||||
repoOnly.isVisible = controller.gitUrl != null
|
||||
cards.show(body, if (loading()) CARD_LOAD else CARD_TABS)
|
||||
revalidate()
|
||||
repaint()
|
||||
@@ -404,6 +420,14 @@ class HistoryPanel(
|
||||
return items.indices.mapNotNull { HistoryRenderer.section(items, it) }
|
||||
}
|
||||
|
||||
internal fun repoOnlyVisible() = repoOnly.isVisible
|
||||
|
||||
internal fun repoOnlySelected() = repoOnly.isSelected
|
||||
|
||||
internal fun clickRepoOnly() {
|
||||
repoOnly.doClick()
|
||||
}
|
||||
|
||||
private fun activeList(): JBList<out HistoryItem> = if (tabs.selectedInfo === cloudInfo) cloudList else localList
|
||||
|
||||
private fun activeModel(): HistoryModel<out HistoryItem> = if (tabs.selectedInfo === cloudInfo) controller.cloud else controller.local
|
||||
@@ -423,7 +447,7 @@ class HistoryPanel(
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
// no-op
|
||||
controller.onRepoOnlyChanged = null
|
||||
}
|
||||
|
||||
private class LoadMoreButton : JButton(KiloBundle.message("history.cloud.load.more")) {
|
||||
|
||||
@@ -96,6 +96,7 @@ history.delete.confirm.message.multiple=Delete {0} sessions from local history?
|
||||
history.rename.title=Rename Session
|
||||
history.rename.prompt=New session name:
|
||||
history.cloud.load.more=Load more
|
||||
history.cloud.repo.only=Only this repository
|
||||
history.group.today=Today
|
||||
history.group.yesterday=Yesterday
|
||||
history.group.week=This Week
|
||||
|
||||
+109
@@ -546,8 +546,117 @@ class HistoryControllerTest : BasePlatformTestCase() {
|
||||
assertEquals(listOf("ses_2"), controller.local.visibleItems.map { it.id })
|
||||
}
|
||||
|
||||
fun `test cloud load passes git url when repo only enabled`() {
|
||||
rpc.cloud += cloud("cloud_1", "Cloud One")
|
||||
val url = "git@github.com:test/repo.git"
|
||||
val controller = controllerWithGit(url)
|
||||
|
||||
controller.reloadCloud()
|
||||
flush()
|
||||
|
||||
assertEquals(1, rpc.cloudCalls.size)
|
||||
assertEquals(url, rpc.cloudCalls[0].gitUrl)
|
||||
assertEquals(true, controller.repoOnly)
|
||||
assertEquals(url, controller.gitUrl)
|
||||
}
|
||||
|
||||
fun `test cloud load passes null when no git url`() {
|
||||
rpc.cloud += cloud("cloud_1", "Cloud One")
|
||||
val controller = controllerWithGit(null)
|
||||
|
||||
controller.reloadCloud()
|
||||
flush()
|
||||
|
||||
assertEquals(1, rpc.cloudCalls.size)
|
||||
assertNull(rpc.cloudCalls[0].gitUrl)
|
||||
assertEquals(false, controller.repoOnly)
|
||||
assertNull(controller.gitUrl)
|
||||
}
|
||||
|
||||
fun `test cloud load passes null when repo only disabled`() {
|
||||
rpc.cloud += cloud("cloud_1", "Cloud One")
|
||||
val url = "git@github.com:test/repo.git"
|
||||
val controller = controllerWithGit(url)
|
||||
|
||||
controller.reloadCloud()
|
||||
flush()
|
||||
assertEquals(url, rpc.cloudCalls[0].gitUrl)
|
||||
|
||||
controller.applyRepoOnly(false)
|
||||
flush()
|
||||
|
||||
assertEquals(2, rpc.cloudCalls.size)
|
||||
assertNull(rpc.cloudCalls[1].gitUrl)
|
||||
}
|
||||
|
||||
fun `test load more passes git url when repo only enabled`() {
|
||||
rpc.cloud += cloud("cloud_1", "Cloud One")
|
||||
rpc.cloudCursor = "next_1"
|
||||
val url = "git@github.com:test/repo.git"
|
||||
val controller = controllerWithGit(url)
|
||||
|
||||
controller.reloadCloud()
|
||||
flush()
|
||||
|
||||
rpc.cloud.clear()
|
||||
rpc.cloud += cloud("cloud_2", "Cloud Two")
|
||||
rpc.cloudCursor = null
|
||||
controller.loadMoreCloud()
|
||||
flush()
|
||||
|
||||
assertEquals(2, rpc.cloudCalls.size)
|
||||
assertEquals(url, rpc.cloudCalls[1].gitUrl)
|
||||
}
|
||||
|
||||
fun `test repo only checkbox visible only when git url exists`() {
|
||||
val url = "git@github.com:test/repo.git"
|
||||
val panel = HistoryPanel(parent, controllerWithGit(url))
|
||||
flush()
|
||||
|
||||
panel.clickCloud()
|
||||
flush()
|
||||
|
||||
assertTrue(panel.repoOnlyVisible())
|
||||
assertTrue(panel.repoOnlySelected())
|
||||
}
|
||||
|
||||
fun `test repo only checkbox hidden when no git url`() {
|
||||
val panel = HistoryPanel(parent, controllerWithGit(null))
|
||||
flush()
|
||||
|
||||
panel.clickCloud()
|
||||
flush()
|
||||
|
||||
assertFalse(panel.repoOnlyVisible())
|
||||
}
|
||||
|
||||
fun `test repo only checkbox toggle reloads cloud history`() {
|
||||
rpc.cloud += cloud("cloud_1", "Cloud One")
|
||||
val url = "git@github.com:test/repo.git"
|
||||
val panel = HistoryPanel(parent, controllerWithGit(url))
|
||||
flush()
|
||||
|
||||
panel.clickCloud()
|
||||
flush()
|
||||
|
||||
val before = rpc.cloudCalls.size
|
||||
|
||||
panel.clickRepoOnly()
|
||||
flush()
|
||||
|
||||
assertTrue(rpc.cloudCalls.size > before)
|
||||
assertFalse(panel.repoOnlySelected())
|
||||
}
|
||||
|
||||
private fun controller() = HistoryController(sessions, workspace, scope)
|
||||
|
||||
private fun controllerWithGit(url: String?) = HistoryController(
|
||||
sessions,
|
||||
workspace,
|
||||
scope,
|
||||
gitUrlProvider = { url },
|
||||
)
|
||||
|
||||
private fun controller(opened: MutableList<String>) = HistoryController(sessions, workspace, scope, open = { open ->
|
||||
val id = when (open) {
|
||||
is SessionRef.Local -> open.id
|
||||
|
||||
Reference in New Issue
Block a user