mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IJPL-233558 remove order
GitOrigin-RevId: 50d8ba849a0f1747bb4c6ac4a1e7e4163b9d9208
This commit is contained in:
committed by
intellij-monorepo-bot
parent
4c2f36606e
commit
15ca8a4acf
+3
-1
@@ -7,6 +7,8 @@
|
||||
<!-- endregion -->
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<agent.workbench.sessionProviderBridge implementation="com.intellij.agent.workbench.claude.sessions.ClaudeAgentSessionProviderBridge"/>
|
||||
<agent.workbench.sessionProviderBridge
|
||||
id="agentWorkbenchClaude"
|
||||
implementation="com.intellij.agent.workbench.claude.sessions.ClaudeAgentSessionProviderBridge"/>
|
||||
</extensions>
|
||||
</idea-plugin>
|
||||
|
||||
@@ -11,9 +11,6 @@ import com.intellij.agent.workbench.sessions.providers.AgentSessionSource
|
||||
internal class ClaudeAgentSessionProviderBridge(
|
||||
override val sessionSource: AgentSessionSource = ClaudeSessionSource(),
|
||||
) : AgentSessionProviderBridge {
|
||||
override val order: Int
|
||||
get() = 10
|
||||
|
||||
override val provider: AgentSessionProvider
|
||||
get() = AgentSessionProvider.CLAUDE
|
||||
|
||||
|
||||
+4
-2
@@ -8,7 +8,9 @@
|
||||
<!-- endregion -->
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<agent.workbench.sessionProviderBridge implementation="com.intellij.agent.workbench.codex.sessions.CodexAgentSessionProviderBridge"/>
|
||||
<agent.workbench.sessionProviderBridge
|
||||
id="agentWorkbenchCodex"
|
||||
order="before agentWorkbenchClaude"
|
||||
implementation="com.intellij.agent.workbench.codex.sessions.CodexAgentSessionProviderBridge"/>
|
||||
</extensions>
|
||||
|
||||
</idea-plugin>
|
||||
|
||||
@@ -15,9 +15,6 @@ import com.intellij.openapi.components.service
|
||||
internal class CodexAgentSessionProviderBridge(
|
||||
override val sessionSource: AgentSessionSource = CodexSessionSource(),
|
||||
) : AgentSessionProviderBridge {
|
||||
override val order: Int
|
||||
get() = 20
|
||||
|
||||
override val provider: AgentSessionProvider
|
||||
get() = AgentSessionProvider.CODEX
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ data class AgentSessionThreadPreview(
|
||||
@JvmField val id: String,
|
||||
@JvmField val title: String,
|
||||
@JvmField val updatedAt: Long,
|
||||
val provider: AgentSessionProvider = AgentSessionProvider.CODEX,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
|
||||
@@ -66,6 +66,7 @@ internal class AgentSessionsService private constructor(
|
||||
private val serviceScope: CoroutineScope,
|
||||
private val sessionSourcesProvider: () -> List<AgentSessionSource>,
|
||||
private val projectEntriesProvider: suspend (AgentSessionsService) -> List<ProjectEntry>,
|
||||
private val treeUiState: SessionsTreeUiState,
|
||||
subscribeToProjectLifecycle: Boolean,
|
||||
) {
|
||||
@Suppress("unused")
|
||||
@@ -73,6 +74,7 @@ internal class AgentSessionsService private constructor(
|
||||
serviceScope = serviceScope,
|
||||
sessionSourcesProvider = AgentSessionProviderBridges::sessionSources,
|
||||
projectEntriesProvider = { service -> service.collectProjects() },
|
||||
treeUiState = service<AgentSessionsTreeUiStateService>(),
|
||||
subscribeToProjectLifecycle = true,
|
||||
)
|
||||
|
||||
@@ -80,11 +82,13 @@ internal class AgentSessionsService private constructor(
|
||||
serviceScope: CoroutineScope,
|
||||
sessionSourcesProvider: () -> List<AgentSessionSource>,
|
||||
projectEntriesProvider: suspend () -> List<ProjectEntry>,
|
||||
treeUiState: SessionsTreeUiState = InMemorySessionsTreeUiState(),
|
||||
subscribeToProjectLifecycle: Boolean = false,
|
||||
) : this(
|
||||
serviceScope = serviceScope,
|
||||
sessionSourcesProvider = sessionSourcesProvider,
|
||||
projectEntriesProvider = { _ -> projectEntriesProvider() },
|
||||
treeUiState = treeUiState,
|
||||
subscribeToProjectLifecycle = subscribeToProjectLifecycle,
|
||||
)
|
||||
|
||||
@@ -280,49 +284,82 @@ internal class AgentSessionsService private constructor(
|
||||
}
|
||||
try {
|
||||
val entries = projectEntriesProvider(this@AgentSessionsService)
|
||||
val currentProjectsByPath = mutableState.value.projects.associateBy { it.path }
|
||||
val currentState = mutableState.value
|
||||
val currentProjectsByPath = currentState.projects.associateBy { normalizePath(it.path) }
|
||||
val openPaths = entries.flatMap { entry ->
|
||||
buildList {
|
||||
if (entry.project != null) add(normalizePath(entry.path))
|
||||
entry.worktreeEntries.filter { it.project != null }.forEach { add(normalizePath(it.path)) }
|
||||
}
|
||||
}
|
||||
treeUiState.retainOpenProjectThreadPreviews(openPaths.toSet())
|
||||
val knownPaths = entries.flatMap { entry ->
|
||||
buildList {
|
||||
add(normalizePath(entry.path))
|
||||
entry.worktreeEntries.forEach { add(normalizePath(it.path)) }
|
||||
}
|
||||
}
|
||||
val initialVisibleThreadCounts = buildInitialVisibleThreadCounts(
|
||||
knownPaths = knownPaths,
|
||||
currentVisibleThreadCounts = currentState.visibleThreadCounts,
|
||||
)
|
||||
val initialProjects = entries.map { entry ->
|
||||
val existing = currentProjectsByPath[entry.path]
|
||||
val normalizedEntryPath = normalizePath(entry.path)
|
||||
val existing = currentProjectsByPath[normalizedEntryPath]
|
||||
val cachedPreviews = if (entry.project != null) {
|
||||
treeUiState.getOpenProjectThreadPreviews(normalizedEntryPath)
|
||||
}
|
||||
else {
|
||||
null
|
||||
}
|
||||
val cachedThreads = cachedPreviews.orEmpty().toCachedSessionThreads()
|
||||
AgentProjectSessions(
|
||||
path = entry.path,
|
||||
path = normalizedEntryPath,
|
||||
name = entry.name,
|
||||
branch = entry.branch ?: existing?.branch,
|
||||
isOpen = entry.project != null,
|
||||
isLoading = entry.project != null,
|
||||
hasLoaded = existing?.hasLoaded ?: false,
|
||||
hasLoaded = existing?.hasLoaded ?: (cachedPreviews != null),
|
||||
hasUnknownThreadCount = existing?.hasUnknownThreadCount ?: false,
|
||||
threads = existing?.threads ?: emptyList(),
|
||||
threads = existing?.threads ?: cachedThreads,
|
||||
errorMessage = existing?.errorMessage,
|
||||
providerWarnings = existing?.providerWarnings ?: emptyList(),
|
||||
worktrees = entry.worktreeEntries.map { wt ->
|
||||
val existingWt = existing?.worktrees?.firstOrNull { it.path == wt.path }
|
||||
val normalizedWorktreePath = normalizePath(wt.path)
|
||||
val existingWt = existing?.worktrees?.firstOrNull { normalizePath(it.path) == normalizedWorktreePath }
|
||||
val cachedWorktreePreviews = if (wt.project != null) {
|
||||
treeUiState.getOpenProjectThreadPreviews(normalizedWorktreePath)
|
||||
}
|
||||
else {
|
||||
null
|
||||
}
|
||||
val cachedWorktreeThreads = cachedWorktreePreviews.orEmpty().toCachedSessionThreads()
|
||||
val hasExistingData = existingWt != null && existingWt.threads.isNotEmpty()
|
||||
AgentWorktree(
|
||||
path = wt.path,
|
||||
path = normalizedWorktreePath,
|
||||
name = wt.name,
|
||||
branch = wt.branch,
|
||||
isOpen = wt.project != null,
|
||||
isLoading = wt.project != null && hasExistingData,
|
||||
hasLoaded = existingWt?.hasLoaded ?: false,
|
||||
isLoading = wt.project != null && (hasExistingData || cachedWorktreePreviews != null),
|
||||
hasLoaded = existingWt?.hasLoaded ?: (cachedWorktreePreviews != null),
|
||||
hasUnknownThreadCount = existingWt?.hasUnknownThreadCount ?: false,
|
||||
threads = existingWt?.threads ?: emptyList(),
|
||||
threads = existingWt?.threads ?: cachedWorktreeThreads,
|
||||
errorMessage = existingWt?.errorMessage,
|
||||
providerWarnings = existingWt?.providerWarnings ?: emptyList(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
mutableState.update { it.copy(projects = initialProjects, lastUpdatedAt = System.currentTimeMillis()) }
|
||||
|
||||
// Collect all open paths for prefetching (batch Codex calls)
|
||||
val sessionSources = sessionSourcesProvider()
|
||||
val openPaths = entries.flatMap { entry ->
|
||||
buildList {
|
||||
if (entry.project != null) add(entry.path)
|
||||
entry.worktreeEntries.filter { it.project != null }.forEach { add(it.path) }
|
||||
}
|
||||
mutableState.update {
|
||||
it.copy(
|
||||
projects = initialProjects,
|
||||
visibleThreadCounts = initialVisibleThreadCounts,
|
||||
lastUpdatedAt = System.currentTimeMillis(),
|
||||
)
|
||||
}
|
||||
|
||||
val sessionSources = sessionSourcesProvider()
|
||||
|
||||
// Prefetch from all sources in parallel
|
||||
val prefetchedByProvider = coroutineScope {
|
||||
sessionSources.map { source ->
|
||||
@@ -342,8 +379,9 @@ internal class AgentSessionsService private constructor(
|
||||
coroutineScope {
|
||||
for (entry in entries) {
|
||||
launch {
|
||||
val normalizedEntryPath = normalizePath(entry.path)
|
||||
if (entry.project == null) {
|
||||
updateProject(entry.path) { it.copy(isLoading = false) }
|
||||
updateProject(normalizedEntryPath) { it.copy(isLoading = false) }
|
||||
return@launch
|
||||
}
|
||||
val sourceResults = java.util.concurrent.CopyOnWriteArrayList<AgentSessionSourceLoadResult>()
|
||||
@@ -351,9 +389,9 @@ internal class AgentSessionsService private constructor(
|
||||
for (source in sessionSources) {
|
||||
launch {
|
||||
val sourceResult = try {
|
||||
val prefetched = prefetchedByProvider[source.provider]?.get(entry.path)
|
||||
val prefetched = prefetchedByProvider[source.provider]?.get(normalizedEntryPath)
|
||||
val threads = prefetched
|
||||
?: source.listThreadsFromOpenProject(path = entry.path, project = entry.project)
|
||||
?: source.listThreadsFromOpenProject(path = normalizedEntryPath, project = entry.project)
|
||||
AgentSessionSourceLoadResult(
|
||||
provider = source.provider,
|
||||
result = Result.success(threads),
|
||||
@@ -376,7 +414,7 @@ internal class AgentSessionsService private constructor(
|
||||
resolveWarningMessage = ::resolveProviderWarningMessage,
|
||||
)
|
||||
val anySuccess = sourceResults.any { it.result.isSuccess }
|
||||
updateProject(entry.path) { project ->
|
||||
updateProject(normalizedEntryPath) { project ->
|
||||
project.copy(
|
||||
threads = partial.threads,
|
||||
providerWarnings = partial.providerWarnings,
|
||||
@@ -392,7 +430,7 @@ internal class AgentSessionsService private constructor(
|
||||
resolveErrorMessage = ::resolveErrorMessage,
|
||||
resolveWarningMessage = ::resolveProviderWarningMessage,
|
||||
)
|
||||
updateProject(entry.path) { project ->
|
||||
updateProject(normalizedEntryPath) { project ->
|
||||
project.copy(isLoading = false,
|
||||
hasLoaded = true,
|
||||
hasUnknownThreadCount = finalResult.hasUnknownThreadCount,
|
||||
@@ -400,11 +438,16 @@ internal class AgentSessionsService private constructor(
|
||||
errorMessage = finalResult.errorMessage,
|
||||
providerWarnings = finalResult.providerWarnings)
|
||||
}
|
||||
if (finalResult.errorMessage == null) {
|
||||
treeUiState.setOpenProjectThreadPreviews(normalizedEntryPath, finalResult.threads.toThreadPreviews())
|
||||
}
|
||||
}
|
||||
for (wt in entry.worktreeEntries) {
|
||||
launch {
|
||||
val normalizedEntryPath = normalizePath(entry.path)
|
||||
val normalizedWorktreePath = normalizePath(wt.path)
|
||||
if (wt.project == null) {
|
||||
updateWorktree(entry.path, wt.path) { it.copy(isLoading = false) }
|
||||
updateWorktree(normalizedEntryPath, normalizedWorktreePath) { it.copy(isLoading = false) }
|
||||
return@launch
|
||||
}
|
||||
val sourceResults = java.util.concurrent.CopyOnWriteArrayList<AgentSessionSourceLoadResult>()
|
||||
@@ -412,9 +455,9 @@ internal class AgentSessionsService private constructor(
|
||||
for (source in sessionSources) {
|
||||
launch {
|
||||
val sourceResult = try {
|
||||
val prefetched = prefetchedByProvider[source.provider]?.get(wt.path)
|
||||
val prefetched = prefetchedByProvider[source.provider]?.get(normalizedWorktreePath)
|
||||
val threads = prefetched
|
||||
?: source.listThreadsFromOpenProject(path = wt.path, project = wt.project)
|
||||
?: source.listThreadsFromOpenProject(path = normalizedWorktreePath, project = wt.project)
|
||||
AgentSessionSourceLoadResult(
|
||||
provider = source.provider,
|
||||
result = Result.success(threads),
|
||||
@@ -436,7 +479,7 @@ internal class AgentSessionsService private constructor(
|
||||
resolveWarningMessage = ::resolveProviderWarningMessage,
|
||||
)
|
||||
val anySuccess = sourceResults.any { it.result.isSuccess }
|
||||
updateWorktree(entry.path, wt.path) { worktree ->
|
||||
updateWorktree(normalizedEntryPath, normalizedWorktreePath) { worktree ->
|
||||
worktree.copy(
|
||||
threads = partial.threads,
|
||||
providerWarnings = partial.providerWarnings,
|
||||
@@ -451,7 +494,7 @@ internal class AgentSessionsService private constructor(
|
||||
resolveErrorMessage = ::resolveErrorMessage,
|
||||
resolveWarningMessage = ::resolveProviderWarningMessage,
|
||||
)
|
||||
updateWorktree(entry.path, wt.path) { worktree ->
|
||||
updateWorktree(normalizedEntryPath, normalizedWorktreePath) { worktree ->
|
||||
worktree.copy(isLoading = false,
|
||||
hasLoaded = true,
|
||||
hasUnknownThreadCount = finalResult.hasUnknownThreadCount,
|
||||
@@ -459,6 +502,9 @@ internal class AgentSessionsService private constructor(
|
||||
errorMessage = finalResult.errorMessage,
|
||||
providerWarnings = finalResult.providerWarnings)
|
||||
}
|
||||
if (finalResult.errorMessage == null) {
|
||||
treeUiState.setOpenProjectThreadPreviews(normalizedWorktreePath, finalResult.threads.toThreadPreviews())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -510,14 +556,22 @@ internal class AgentSessionsService private constructor(
|
||||
}
|
||||
|
||||
fun showMoreThreads(path: String) {
|
||||
val normalizedPath = normalizePath(path)
|
||||
var deltaToPersist = 0
|
||||
mutableState.update { state ->
|
||||
val current = state.visibleThreadCounts[path] ?: DEFAULT_VISIBLE_THREAD_COUNT
|
||||
state.copy(visibleThreadCounts = state.visibleThreadCounts + (path to (current + DEFAULT_VISIBLE_THREAD_COUNT)))
|
||||
val current = state.visibleThreadCounts[normalizedPath] ?: treeUiState.getVisibleThreadCount(normalizedPath)
|
||||
val nextVisible = current + DEFAULT_VISIBLE_THREAD_COUNT
|
||||
deltaToPersist = nextVisible - current
|
||||
state.copy(visibleThreadCounts = state.visibleThreadCounts + (normalizedPath to nextVisible))
|
||||
}
|
||||
if (deltaToPersist > 0) {
|
||||
treeUiState.incrementVisibleThreadCount(normalizedPath, deltaToPersist)
|
||||
}
|
||||
}
|
||||
|
||||
fun ensureThreadVisible(path: String, provider: AgentSessionProvider, threadId: String) {
|
||||
val normalizedPath = normalizePath(path)
|
||||
var deltaToPersist = 0
|
||||
mutableState.update { state ->
|
||||
val threadIndex = findThreadIndex(
|
||||
projects = state.projects,
|
||||
@@ -525,7 +579,7 @@ internal class AgentSessionsService private constructor(
|
||||
provider = provider,
|
||||
threadId = threadId,
|
||||
) ?: return@update state
|
||||
val currentVisible = state.visibleThreadCounts[normalizedPath] ?: DEFAULT_VISIBLE_THREAD_COUNT
|
||||
val currentVisible = state.visibleThreadCounts[normalizedPath] ?: treeUiState.getVisibleThreadCount(normalizedPath)
|
||||
if (threadIndex < currentVisible) {
|
||||
return@update state
|
||||
}
|
||||
@@ -534,8 +588,12 @@ internal class AgentSessionsService private constructor(
|
||||
while (nextVisible < minVisible) {
|
||||
nextVisible += DEFAULT_VISIBLE_THREAD_COUNT
|
||||
}
|
||||
deltaToPersist = nextVisible - currentVisible
|
||||
state.copy(visibleThreadCounts = state.visibleThreadCounts + (normalizedPath to nextVisible))
|
||||
}
|
||||
if (deltaToPersist > 0) {
|
||||
treeUiState.incrementVisibleThreadCount(normalizedPath, deltaToPersist)
|
||||
}
|
||||
}
|
||||
|
||||
fun openChatThread(path: String, thread: AgentSessionThread, currentProject: Project? = null) {
|
||||
@@ -1396,6 +1454,51 @@ internal class AgentSessionsService private constructor(
|
||||
return null
|
||||
}
|
||||
|
||||
private fun buildInitialVisibleThreadCounts(
|
||||
knownPaths: List<String>,
|
||||
currentVisibleThreadCounts: Map<String, Int>,
|
||||
): Map<String, Int> {
|
||||
val normalizedKnownPaths = knownPaths.mapTo(LinkedHashSet()) { normalizePath(it) }
|
||||
val visibleThreadCounts = LinkedHashMap<String, Int>()
|
||||
currentVisibleThreadCounts.forEach { (path, count) ->
|
||||
val normalized = normalizePath(path)
|
||||
if (normalized in normalizedKnownPaths && count > DEFAULT_VISIBLE_THREAD_COUNT) {
|
||||
visibleThreadCounts[normalized] = count
|
||||
}
|
||||
}
|
||||
for (path in normalizedKnownPaths) {
|
||||
if (path in visibleThreadCounts) continue
|
||||
val persisted = treeUiState.getVisibleThreadCount(path)
|
||||
if (persisted > DEFAULT_VISIBLE_THREAD_COUNT) {
|
||||
visibleThreadCounts[path] = persisted
|
||||
}
|
||||
}
|
||||
return visibleThreadCounts
|
||||
}
|
||||
|
||||
private fun List<AgentSessionThreadPreview>.toCachedSessionThreads(): List<AgentSessionThread> {
|
||||
return map { preview ->
|
||||
AgentSessionThread(
|
||||
id = preview.id,
|
||||
title = preview.title,
|
||||
updatedAt = preview.updatedAt,
|
||||
archived = false,
|
||||
provider = preview.provider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<AgentSessionThread>.toThreadPreviews(): List<AgentSessionThreadPreview> {
|
||||
return map { thread ->
|
||||
AgentSessionThreadPreview(
|
||||
id = thread.id,
|
||||
title = thread.title,
|
||||
updatedAt = thread.updatedAt,
|
||||
provider = thread.provider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class ProviderRefreshOutcome(
|
||||
val threads: List<AgentSessionThread>? = null,
|
||||
val warningMessage: String? = null,
|
||||
|
||||
@@ -84,6 +84,7 @@ internal fun agentSessionsToolWindow(currentProject: Project) {
|
||||
onOpenThread = { path, thread -> service.openChatThread(path, thread, currentProject) },
|
||||
onOpenSubAgent = { path, thread, subAgent -> service.openChatSubAgent(path, thread, subAgent, currentProject) },
|
||||
onCreateSession = { path, provider, mode -> service.createNewSession(path, provider, mode, currentProject) },
|
||||
treeUiState = uiStateService,
|
||||
lastUsedProvider = lastUsedProvider,
|
||||
visibleProjectCount = state.visibleProjectCount,
|
||||
onShowMoreProjects = { service.showMoreProjects() },
|
||||
@@ -111,6 +112,7 @@ internal fun agentSessionsToolWindowContent(
|
||||
onOpenThread: (String, AgentSessionThread) -> Unit = { _, _ -> },
|
||||
onOpenSubAgent: (String, AgentSessionThread, AgentSubAgent) -> Unit = { _, _, _ -> },
|
||||
onCreateSession: (String, AgentSessionProvider, AgentSessionLaunchMode) -> Unit = { _, _, _ -> },
|
||||
treeUiState: SessionsTreeUiState? = null,
|
||||
lastUsedProvider: AgentSessionProvider? = null,
|
||||
nowProvider: () -> Long = { System.currentTimeMillis() },
|
||||
visibleProjectCount: Int = Int.MAX_VALUE,
|
||||
@@ -122,6 +124,7 @@ internal fun agentSessionsToolWindowContent(
|
||||
onEnableClaudeQuotaWidget: () -> Unit = {},
|
||||
onDismissClaudeQuotaHint: () -> Unit = {},
|
||||
) {
|
||||
val effectiveTreeUiState = treeUiState ?: remember { InMemorySessionsTreeUiState() }
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -146,6 +149,7 @@ internal fun agentSessionsToolWindowContent(
|
||||
onOpenThread = onOpenThread,
|
||||
onOpenSubAgent = onOpenSubAgent,
|
||||
onCreateSession = onCreateSession,
|
||||
treeUiState = effectiveTreeUiState,
|
||||
lastUsedProvider = lastUsedProvider,
|
||||
nowProvider = nowProvider,
|
||||
visibleProjectCount = visibleProjectCount,
|
||||
|
||||
@@ -158,6 +158,9 @@ internal class AgentSessionsTreeUiStateService
|
||||
id = preview.id,
|
||||
title = preview.title,
|
||||
updatedAt = preview.updatedAt,
|
||||
provider = preview.provider
|
||||
?.let(AgentSessionProvider::fromOrNull)
|
||||
?: AgentSessionProvider.CODEX,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -165,7 +168,14 @@ internal class AgentSessionsTreeUiStateService
|
||||
override fun setOpenProjectThreadPreviews(path: String, threads: List<AgentSessionThreadPreview>): Boolean {
|
||||
val normalizedPath = normalizeSessionsProjectPath(path)
|
||||
val normalizedPreviews = normalizeOpenProjectThreadPreviewList(threads)
|
||||
.map { thread -> ThreadPreviewState(id = thread.id, title = thread.title, updatedAt = thread.updatedAt) }
|
||||
.map { thread ->
|
||||
ThreadPreviewState(
|
||||
id = thread.id,
|
||||
title = thread.title,
|
||||
updatedAt = thread.updatedAt,
|
||||
provider = thread.provider.value,
|
||||
)
|
||||
}
|
||||
val current = state.openProjectThreadPreviewsByProject[normalizedPath]
|
||||
if (current == normalizedPreviews) {
|
||||
return false
|
||||
@@ -278,6 +288,7 @@ internal class AgentSessionsTreeUiStateService
|
||||
@JvmField val id: String,
|
||||
@JvmField val title: String,
|
||||
@JvmField val updatedAt: Long,
|
||||
@JvmField val provider: String? = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ internal fun sessionTree(
|
||||
onOpenThread: (String, AgentSessionThread) -> Unit,
|
||||
onOpenSubAgent: (String, AgentSessionThread, AgentSubAgent) -> Unit,
|
||||
onCreateSession: (String, AgentSessionProvider, AgentSessionLaunchMode) -> Unit = { _, _, _ -> },
|
||||
treeUiState: SessionsTreeUiState,
|
||||
lastUsedProvider: AgentSessionProvider? = null,
|
||||
nowProvider: () -> Long,
|
||||
visibleProjectCount: Int = Int.MAX_VALUE,
|
||||
@@ -41,12 +42,17 @@ internal fun sessionTree(
|
||||
selectedTreeId: SessionTreeId? = null,
|
||||
) {
|
||||
val stateHolder = rememberSessionTreeStateHolder(
|
||||
onProjectExpanded = onProjectExpanded,
|
||||
onProjectCollapsed = {},
|
||||
onProjectExpanded = { path ->
|
||||
treeUiState.setProjectCollapsed(path, collapsed = false)
|
||||
onProjectExpanded(path)
|
||||
},
|
||||
onProjectCollapsed = { path ->
|
||||
treeUiState.setProjectCollapsed(path, collapsed = true)
|
||||
},
|
||||
onWorktreeExpanded = onWorktreeExpanded,
|
||||
)
|
||||
val treeState = stateHolder.treeState
|
||||
val autoOpenNodes = remember(projects, visibleProjectCount) {
|
||||
val autoOpenNodes = remember(projects, visibleProjectCount, treeUiState) {
|
||||
projects.take(visibleProjectCount)
|
||||
.filter {
|
||||
it.isOpen ||
|
||||
@@ -54,6 +60,7 @@ internal fun sessionTree(
|
||||
it.providerWarnings.isNotEmpty() ||
|
||||
it.worktrees.any { wt -> wt.isOpen }
|
||||
}
|
||||
.filterNot { treeUiState.isProjectCollapsed(it.path) }
|
||||
.map { SessionTreeId.Project(it.path) }
|
||||
}
|
||||
LaunchedEffect(autoOpenNodes) {
|
||||
|
||||
@@ -5,9 +5,6 @@ import com.intellij.agent.workbench.sessions.AgentSessionLaunchMode
|
||||
import com.intellij.agent.workbench.sessions.AgentSessionProvider
|
||||
|
||||
interface AgentSessionProviderBridge {
|
||||
val order: Int
|
||||
get() = 0
|
||||
|
||||
val provider: AgentSessionProvider
|
||||
val displayNameKey: String
|
||||
val newSessionLabelKey: String
|
||||
|
||||
@@ -50,11 +50,10 @@ private fun buildAgentSessionProviderBridgeSnapshot(bridges: Iterable<AgentSessi
|
||||
uniqueBridges += bridge
|
||||
}
|
||||
}
|
||||
val orderedBridges = uniqueBridges.sortedBy { it.order }
|
||||
return AgentSessionProviderBridgeSnapshot(
|
||||
bridgesByProvider = bridgesByProvider,
|
||||
orderedBridges = orderedBridges,
|
||||
sessionSources = orderedBridges.map { it.sessionSource },
|
||||
orderedBridges = uniqueBridges,
|
||||
sessionSources = uniqueBridges.map { it.sessionSource },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.intellij.agent.workbench.sessions.providers.AgentSessionProviderBridg
|
||||
import com.intellij.agent.workbench.sessions.providers.AgentSessionSource
|
||||
import com.intellij.agent.workbench.sessions.providers.InMemoryAgentSessionProviderRegistry
|
||||
import com.intellij.openapi.extensions.ExtensionPointName
|
||||
import com.intellij.openapi.extensions.LoadingOrder
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.testFramework.junit5.TestApplication
|
||||
@@ -72,16 +73,16 @@ class AgentSessionProviderBridgesTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun allBridgesAreSortedByOrder() {
|
||||
fun allBridgesFollowExtensionOrder() {
|
||||
val disposable = Disposer.newDisposable()
|
||||
try {
|
||||
val highOrder = TestAgentSessionProviderBridge(provider = AgentSessionProvider.from("aaa"), sourceId = "high", order = 200)
|
||||
val lowOrder = TestAgentSessionProviderBridge(provider = AgentSessionProvider.from("bbb"), sourceId = "low", order = -5)
|
||||
extensionPoint.point.registerExtension(highOrder, disposable)
|
||||
extensionPoint.point.registerExtension(lowOrder, disposable)
|
||||
val lastBridge = TestAgentSessionProviderBridge(provider = AgentSessionProvider.from("aaa"), sourceId = "last")
|
||||
val firstBridge = TestAgentSessionProviderBridge(provider = AgentSessionProvider.from("bbb"), sourceId = "first")
|
||||
extensionPoint.point.registerExtension(lastBridge, LoadingOrder.LAST, disposable)
|
||||
extensionPoint.point.registerExtension(firstBridge, LoadingOrder.FIRST, disposable)
|
||||
|
||||
val orderedIds = AgentSessionProviderBridges.allBridges().map { it.provider.value }
|
||||
assertThat(orderedIds.indexOf(lowOrder.provider.value)).isLessThan(orderedIds.indexOf(highOrder.provider.value))
|
||||
assertThat(orderedIds.indexOf(firstBridge.provider.value)).isLessThan(orderedIds.indexOf(lastBridge.provider.value))
|
||||
}
|
||||
finally {
|
||||
Disposer.dispose(disposable)
|
||||
@@ -90,9 +91,9 @@ class AgentSessionProviderBridgesTest {
|
||||
|
||||
@Test
|
||||
fun registryCanBeOverriddenForIsolatedTests() {
|
||||
val codexBridge = TestAgentSessionProviderBridge(provider = AgentSessionProvider.CODEX, sourceId = "override-codex", order = 50)
|
||||
val claudeBridge = TestAgentSessionProviderBridge(provider = AgentSessionProvider.CLAUDE, sourceId = "override-claude", order = -50)
|
||||
val overrideRegistry = InMemoryAgentSessionProviderRegistry(listOf(codexBridge, claudeBridge))
|
||||
val codexBridge = TestAgentSessionProviderBridge(provider = AgentSessionProvider.CODEX, sourceId = "override-codex")
|
||||
val claudeBridge = TestAgentSessionProviderBridge(provider = AgentSessionProvider.CLAUDE, sourceId = "override-claude")
|
||||
val overrideRegistry = InMemoryAgentSessionProviderRegistry(listOf(claudeBridge, codexBridge))
|
||||
val baselineClaude = AgentSessionProviderBridges.find(AgentSessionProvider.CLAUDE)
|
||||
|
||||
AgentSessionProviderBridges.withRegistryForTest(overrideRegistry) {
|
||||
@@ -108,7 +109,6 @@ class AgentSessionProviderBridgesTest {
|
||||
private class TestAgentSessionProviderBridge(
|
||||
override val provider: AgentSessionProvider,
|
||||
sourceId: String,
|
||||
override val order: Int = 0,
|
||||
) : AgentSessionProviderBridge {
|
||||
override val sessionSource: AgentSessionSource = TestAgentSessionSource(provider, sourceId)
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ internal fun thread(id: String, updatedAt: Long, provider: AgentSessionProvider)
|
||||
internal suspend fun withService(
|
||||
sessionSourcesProvider: () -> List<AgentSessionSource>,
|
||||
projectEntriesProvider: suspend () -> List<AgentSessionsService.ProjectEntry>,
|
||||
treeUiState: SessionsTreeUiState = InMemorySessionsTreeUiState(),
|
||||
action: suspend (AgentSessionsService) -> Unit,
|
||||
) {
|
||||
@Suppress("RAW_SCOPE_CREATION")
|
||||
@@ -55,6 +56,7 @@ internal suspend fun withService(
|
||||
serviceScope = scope,
|
||||
sessionSourcesProvider = sessionSourcesProvider,
|
||||
projectEntriesProvider = projectEntriesProvider,
|
||||
treeUiState = treeUiState,
|
||||
subscribeToProjectLifecycle = false,
|
||||
)
|
||||
action(service)
|
||||
|
||||
+35
@@ -12,6 +12,7 @@ import java.util.concurrent.atomic.AtomicInteger
|
||||
class AgentSessionsServiceOnDemandIntegrationTest {
|
||||
@Test
|
||||
fun ensureThreadVisibleExpandsProjectVisibleCountForHiddenThread() = runBlocking {
|
||||
val treeUiState = InMemorySessionsTreeUiState()
|
||||
withService(
|
||||
sessionSourcesProvider = {
|
||||
listOf(
|
||||
@@ -38,6 +39,7 @@ class AgentSessionsServiceOnDemandIntegrationTest {
|
||||
projectEntriesProvider = {
|
||||
listOf(closedProjectEntry(PROJECT_PATH, "Project A"))
|
||||
},
|
||||
treeUiState = treeUiState,
|
||||
) { service ->
|
||||
service.refresh()
|
||||
waitForCondition {
|
||||
@@ -53,6 +55,39 @@ class AgentSessionsServiceOnDemandIntegrationTest {
|
||||
|
||||
assertThat(service.state.value.visibleThreadCounts[PROJECT_PATH])
|
||||
.isEqualTo(DEFAULT_VISIBLE_THREAD_COUNT + DEFAULT_VISIBLE_THREAD_COUNT)
|
||||
assertThat(treeUiState.getVisibleThreadCount(PROJECT_PATH))
|
||||
.isEqualTo(DEFAULT_VISIBLE_THREAD_COUNT + DEFAULT_VISIBLE_THREAD_COUNT)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun showMoreThreadsPersistsVisibleCountToUiState() = runBlocking {
|
||||
val treeUiState = InMemorySessionsTreeUiState()
|
||||
withService(
|
||||
sessionSourcesProvider = {
|
||||
listOf(
|
||||
ScriptedSessionSource(
|
||||
provider = AgentSessionProvider.CODEX,
|
||||
canReportExactThreadCount = true,
|
||||
),
|
||||
)
|
||||
},
|
||||
projectEntriesProvider = {
|
||||
listOf(closedProjectEntry(PROJECT_PATH, "Project A"))
|
||||
},
|
||||
treeUiState = treeUiState,
|
||||
) { service ->
|
||||
service.refresh()
|
||||
waitForCondition {
|
||||
service.state.value.projects.any { it.path == PROJECT_PATH }
|
||||
}
|
||||
|
||||
service.showMoreThreads(PROJECT_PATH)
|
||||
|
||||
assertThat(service.state.value.visibleThreadCounts[PROJECT_PATH])
|
||||
.isEqualTo(DEFAULT_VISIBLE_THREAD_COUNT + DEFAULT_VISIBLE_THREAD_COUNT)
|
||||
assertThat(treeUiState.getVisibleThreadCount(PROJECT_PATH))
|
||||
.isEqualTo(DEFAULT_VISIBLE_THREAD_COUNT + DEFAULT_VISIBLE_THREAD_COUNT)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+87
@@ -2,6 +2,7 @@
|
||||
package com.intellij.agent.workbench.sessions
|
||||
|
||||
import com.intellij.testFramework.junit5.TestApplication
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
@@ -9,6 +10,92 @@ import org.junit.jupiter.api.Test
|
||||
|
||||
@TestApplication
|
||||
class AgentSessionsServiceRefreshIntegrationTest {
|
||||
@Test
|
||||
fun refreshShowsCachedOpenProjectThreadsBeforeProviderLoadCompletes() = runBlocking {
|
||||
val started = CompletableDeferred<Unit>()
|
||||
val release = CompletableDeferred<Unit>()
|
||||
val treeUiState = InMemorySessionsTreeUiState()
|
||||
treeUiState.setOpenProjectThreadPreviews(
|
||||
PROJECT_PATH,
|
||||
listOf(
|
||||
AgentSessionThreadPreview(
|
||||
id = "cached-1",
|
||||
title = "Cached",
|
||||
updatedAt = 100,
|
||||
provider = AgentSessionProvider.CLAUDE,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
withService(
|
||||
sessionSourcesProvider = {
|
||||
listOf(
|
||||
ScriptedSessionSource(
|
||||
provider = AgentSessionProvider.CLAUDE,
|
||||
listFromOpenProject = { path, _ ->
|
||||
if (path != PROJECT_PATH) {
|
||||
emptyList()
|
||||
}
|
||||
else {
|
||||
started.complete(Unit)
|
||||
release.await()
|
||||
listOf(thread(id = "claude-1", updatedAt = 200, provider = AgentSessionProvider.CLAUDE))
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
},
|
||||
projectEntriesProvider = {
|
||||
listOf(openProjectEntry(PROJECT_PATH, "Project A"))
|
||||
},
|
||||
treeUiState = treeUiState,
|
||||
) { service ->
|
||||
service.refresh()
|
||||
started.await()
|
||||
|
||||
waitForCondition {
|
||||
val project = service.state.value.projects.firstOrNull { it.path == PROJECT_PATH } ?: return@waitForCondition false
|
||||
project.hasLoaded && project.threads.map { it.id } == listOf("cached-1")
|
||||
}
|
||||
|
||||
release.complete(Unit)
|
||||
waitForCondition {
|
||||
service.state.value.projects.firstOrNull { it.path == PROJECT_PATH }?.threads?.map { it.id } == listOf("claude-1")
|
||||
}
|
||||
|
||||
assertThat(treeUiState.getOpenProjectThreadPreviews(PROJECT_PATH).orEmpty().map { it.id })
|
||||
.containsExactly("claude-1")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun refreshRestoresPersistedVisibleThreadCountForKnownPath() = runBlocking {
|
||||
val treeUiState = InMemorySessionsTreeUiState()
|
||||
treeUiState.incrementVisibleThreadCount(PROJECT_PATH, delta = 6)
|
||||
|
||||
withService(
|
||||
sessionSourcesProvider = {
|
||||
listOf(
|
||||
ScriptedSessionSource(
|
||||
provider = AgentSessionProvider.CODEX,
|
||||
)
|
||||
)
|
||||
},
|
||||
projectEntriesProvider = {
|
||||
listOf(closedProjectEntry(PROJECT_PATH, "Project A"))
|
||||
},
|
||||
treeUiState = treeUiState,
|
||||
) { service ->
|
||||
service.refresh()
|
||||
waitForCondition {
|
||||
service.state.value.projects.any { it.path == PROJECT_PATH }
|
||||
}
|
||||
|
||||
assertThat(service.state.value.visibleThreadCounts[PROJECT_PATH])
|
||||
.isEqualTo(DEFAULT_VISIBLE_THREAD_COUNT + 6)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun refreshMergesMixedProviderThreadsAndMarksUnknownCount() = runBlocking {
|
||||
withService(
|
||||
|
||||
@@ -398,6 +398,99 @@ class AgentSessionsToolWindowTest {
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun collapsedProjectInUiStateIsNotAutoExpandedAndExpandingClearsCollapsedFlag() {
|
||||
val projectPath = "/work/project-a"
|
||||
val treeUiState = InMemorySessionsTreeUiState()
|
||||
treeUiState.setProjectCollapsed(projectPath, collapsed = true)
|
||||
val projects = listOf(
|
||||
AgentProjectSessions(
|
||||
path = projectPath,
|
||||
name = "Project A",
|
||||
isOpen = true,
|
||||
hasLoaded = true,
|
||||
),
|
||||
)
|
||||
|
||||
composeRule.setContentWithTheme {
|
||||
agentSessionsToolWindowContent(
|
||||
state = AgentSessionsState(projects = projects),
|
||||
onRefresh = {},
|
||||
onOpenProject = {},
|
||||
treeUiState = treeUiState,
|
||||
)
|
||||
}
|
||||
|
||||
composeRule.onNodeWithText("Project A").assertIsDisplayed()
|
||||
composeRule.onAllNodesWithText(AgentSessionsBundle.message("toolwindow.empty.project"))
|
||||
.assertCountEquals(0)
|
||||
|
||||
composeRule.onNodeWithText("Project A")
|
||||
.assertIsDisplayed()
|
||||
.performMouseInput { doubleClick() }
|
||||
|
||||
composeRule.onNodeWithText(AgentSessionsBundle.message("toolwindow.empty.project"))
|
||||
.assertIsDisplayed()
|
||||
composeRule.runOnIdle {
|
||||
assertThat(treeUiState.isProjectCollapsed(projectPath)).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun collapsedProjectInPersistentUiStateRemainsCollapsedAfterContentRefresh() {
|
||||
val projectPath = "/work/project-a"
|
||||
val treeUiState = AgentSessionsTreeUiStateService()
|
||||
val projects = listOf(
|
||||
AgentProjectSessions(
|
||||
path = projectPath,
|
||||
name = "Project A",
|
||||
isOpen = true,
|
||||
hasLoaded = true,
|
||||
),
|
||||
)
|
||||
|
||||
composeRule.setContentWithTheme {
|
||||
agentSessionsToolWindowContent(
|
||||
state = AgentSessionsState(projects = projects, lastUpdatedAt = 1L),
|
||||
onRefresh = {},
|
||||
onOpenProject = {},
|
||||
treeUiState = treeUiState,
|
||||
)
|
||||
}
|
||||
|
||||
composeRule.onNodeWithText(AgentSessionsBundle.message("toolwindow.empty.project"))
|
||||
.assertIsDisplayed()
|
||||
composeRule.onNodeWithText("Project A")
|
||||
.assertIsDisplayed()
|
||||
.performMouseInput { doubleClick() }
|
||||
composeRule.onAllNodesWithText(AgentSessionsBundle.message("toolwindow.empty.project"))
|
||||
.assertCountEquals(0)
|
||||
composeRule.runOnIdle {
|
||||
assertThat(treeUiState.isProjectCollapsed(projectPath)).isTrue()
|
||||
}
|
||||
|
||||
composeRule.setContentWithTheme {
|
||||
agentSessionsToolWindowContent(
|
||||
state = AgentSessionsState(projects = projects, lastUpdatedAt = 2L),
|
||||
onRefresh = {},
|
||||
onOpenProject = {},
|
||||
treeUiState = treeUiState,
|
||||
)
|
||||
}
|
||||
|
||||
composeRule.onNodeWithText("Project A").assertIsDisplayed()
|
||||
composeRule.onAllNodesWithText(AgentSessionsBundle.message("toolwindow.empty.project"))
|
||||
.assertCountEquals(0)
|
||||
composeRule.onNodeWithText("Project A")
|
||||
.assertIsDisplayed()
|
||||
.performMouseInput { doubleClick() }
|
||||
composeRule.onNodeWithText(AgentSessionsBundle.message("toolwindow.empty.project"))
|
||||
.assertIsDisplayed()
|
||||
composeRule.runOnIdle {
|
||||
assertThat(treeUiState.isProjectCollapsed(projectPath)).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun expandingLoadedEmptyProjectShowsEmptyChildRow() {
|
||||
val now = 1_700_000_000_000L
|
||||
|
||||
@@ -61,6 +61,59 @@ class AgentSessionsTreeUiStateServiceTest {
|
||||
assertNull(uiState.getOpenProjectThreadPreviews("/work/project-a"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun openProjectThreadPreviewCachePreservesProvider() {
|
||||
val uiState = AgentSessionsTreeUiStateService()
|
||||
val threads = listOf(
|
||||
AgentSessionThreadPreview(
|
||||
id = "claude-thread",
|
||||
title = "Claude Thread",
|
||||
updatedAt = 5L,
|
||||
provider = AgentSessionProvider.CLAUDE,
|
||||
)
|
||||
)
|
||||
|
||||
assertTrue(uiState.setOpenProjectThreadPreviews("/work/project-a", threads))
|
||||
assertEquals(
|
||||
AgentSessionProvider.CLAUDE,
|
||||
uiState.getOpenProjectThreadPreviews("/work/project-a")?.single()?.provider,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun treeStateFieldsSurviveServiceStateRoundTrip() {
|
||||
val original = AgentSessionsTreeUiStateService()
|
||||
val path = "/work/project-a/"
|
||||
original.setProjectCollapsed(path, collapsed = true)
|
||||
original.incrementVisibleThreadCount(path, delta = 4)
|
||||
original.setOpenProjectThreadPreviews(
|
||||
path,
|
||||
listOf(
|
||||
AgentSessionThreadPreview(
|
||||
id = "claude-thread",
|
||||
title = "Claude Thread",
|
||||
updatedAt = 20,
|
||||
provider = AgentSessionProvider.CLAUDE,
|
||||
),
|
||||
AgentSessionThreadPreview(
|
||||
id = "codex-thread",
|
||||
title = "Codex Thread",
|
||||
updatedAt = 10,
|
||||
provider = AgentSessionProvider.CODEX,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
val reloaded = AgentSessionsTreeUiStateService()
|
||||
reloaded.loadState(original.state)
|
||||
|
||||
assertTrue(reloaded.isProjectCollapsed("/work/project-a"))
|
||||
assertEquals(DEFAULT_VISIBLE_THREAD_COUNT + 4, reloaded.getVisibleThreadCount("/work/project-a"))
|
||||
val previews = reloaded.getOpenProjectThreadPreviews("/work/project-a").orEmpty()
|
||||
assertEquals(listOf("claude-thread", "codex-thread"), previews.map { it.id })
|
||||
assertEquals(listOf(AgentSessionProvider.CLAUDE, AgentSessionProvider.CODEX), previews.map { it.provider })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun lastUsedProviderDefaultsToNull() {
|
||||
val uiState = AgentSessionsTreeUiStateService()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name: Agent Threads Testing
|
||||
description: Coverage requirements for provider aggregation, tree rendering, and backend contracts in Agent Threads.
|
||||
targets:
|
||||
- ../sessions/src/AgentSessionsTreeUiStateService.kt
|
||||
- ../codex/sessions/testSrc/CodexRolloutSessionBackendTest.kt
|
||||
- ../codex/sessions/testSrc/CodexSessionBackendSelectorTest.kt
|
||||
- ../sessions/testSrc/AgentSessionLoadAggregationTest.kt
|
||||
@@ -10,6 +11,7 @@ targets:
|
||||
- ../sessions/testSrc/AgentSessionsServiceConcurrencyIntegrationTest.kt
|
||||
- ../sessions/testSrc/AgentSessionsServiceIntegrationTestSupport.kt
|
||||
- ../sessions/testSrc/AgentSessionsToolWindowTest.kt
|
||||
- ../sessions/testSrc/AgentSessionsTreeUiStateServiceTest.kt
|
||||
- ../sessions/testSrc/CodexSessionsPagingLogicTest.kt
|
||||
- ../sessions/testSrc/CodexAppServerClientTest.kt
|
||||
- ../sessions/testSrc/CodexAppServerClientTestSupport.kt
|
||||
@@ -19,7 +21,7 @@ targets:
|
||||
# Agent Threads Testing
|
||||
|
||||
Status: Draft
|
||||
Date: 2026-02-11
|
||||
Date: 2026-02-16
|
||||
|
||||
## Summary
|
||||
Define required test coverage for the multi-provider Agent Threads stack: source aggregation, service behavior, tree/UI rendering, and Codex backend protocol compatibility.
|
||||
@@ -43,10 +45,14 @@ Define required test coverage for the multi-provider Agent Threads stack: source
|
||||
- Service integration tests must cover:
|
||||
- mixed-provider refresh merge,
|
||||
- provider warning and blocking error paths,
|
||||
- unknown-count behavior when unknown provider fails/succeeds.
|
||||
- unknown-count behavior when unknown provider fails/succeeds,
|
||||
- cached preview rows rendered before open-path provider load completes,
|
||||
- persisted visible thread count restoration during refresh bootstrap.
|
||||
- On-demand integration tests must cover:
|
||||
- project request deduplication,
|
||||
- worktree request deduplication with refresh interaction.
|
||||
- worktree request deduplication with refresh interaction,
|
||||
- `showMoreThreads` visible-count persistence,
|
||||
- `ensureThreadVisible` visible-count persistence.
|
||||
- Concurrency integration tests must verify refresh mutex deduplicates overlapping refresh calls.
|
||||
- Codex rollout backend tests must cover rollout parsing/activity behavior as the default thread-discovery path.
|
||||
- Codex backend selector tests must verify rollout default behavior and explicit app-server override behavior.
|
||||
@@ -54,7 +60,13 @@ Define required test coverage for the multi-provider Agent Threads stack: source
|
||||
- provider warning rendering,
|
||||
- error row precedence over warnings,
|
||||
- `More…` rendering for unknown count,
|
||||
- `More (N)` rendering for exact count.
|
||||
- `More (N)` rendering for exact count,
|
||||
- persisted collapsed state blocking default auto-expand,
|
||||
- collapsed-state persistence across content refresh/recreation when persistent tree UI state is used.
|
||||
- Tree UI state service tests must cover:
|
||||
- collapsed/visible-count/open-preview state round-trip,
|
||||
- preview provider identity persistence,
|
||||
- backward-compatible provider default for legacy preview entries with missing provider value.
|
||||
- Codex compatibility tests must cover cursor-loop/no-progress guard behavior in `seedInitialVisibleThreads`.
|
||||
- Codex app-server contract tests must run against mock backend always and real backend when available.
|
||||
|
||||
@@ -63,6 +75,7 @@ Define required test coverage for the multi-provider Agent Threads stack: source
|
||||
[@test] ../sessions/testSrc/AgentSessionsServiceOnDemandIntegrationTest.kt
|
||||
[@test] ../sessions/testSrc/AgentSessionsServiceConcurrencyIntegrationTest.kt
|
||||
[@test] ../sessions/testSrc/AgentSessionsToolWindowTest.kt
|
||||
[@test] ../sessions/testSrc/AgentSessionsTreeUiStateServiceTest.kt
|
||||
[@test] ../sessions/testSrc/CodexSessionsPagingLogicTest.kt
|
||||
[@test] ../sessions/testSrc/CodexAppServerClientTest.kt
|
||||
|
||||
|
||||
@@ -8,12 +8,13 @@ targets:
|
||||
- ../sessions/resources/messages/AgentSessionsBundle.properties
|
||||
- ../sessions/testSrc/AgentSessionsToolWindowTest.kt
|
||||
- ../sessions/testSrc/AgentSessionsServiceRefreshIntegrationTest.kt
|
||||
- ../sessions/testSrc/AgentSessionsServiceOnDemandIntegrationTest.kt
|
||||
---
|
||||
|
||||
# Agent Threads Visibility and More Row
|
||||
|
||||
Status: Draft
|
||||
Date: 2026-02-11
|
||||
Date: 2026-02-16
|
||||
|
||||
## Summary
|
||||
Define one consistent visibility model for project/worktree thread rows so empty state, warning/error rows, and `More` rows never conflict.
|
||||
@@ -29,6 +30,10 @@ Define one consistent visibility model for project/worktree thread rows so empty
|
||||
|
||||
## Requirements
|
||||
- Initial visible thread count per path is `DEFAULT_VISIBLE_THREAD_COUNT` (3).
|
||||
- Visible thread count lookup order per normalized path is:
|
||||
- in-memory runtime `visibleThreadCounts` entry,
|
||||
- persisted tree UI state visible count,
|
||||
- default value (`DEFAULT_VISIBLE_THREAD_COUNT`).
|
||||
- For project rows, show `More` row only when `project.threads.size > visibleCount`.
|
||||
- For worktree rows, show `More` row only when `worktree.threads.size > visibleCount`.
|
||||
- If `hasUnknownThreadCount=true` for the corresponding node, `More` row must render without count (`toolwindow.action.more`).
|
||||
@@ -41,15 +46,21 @@ Define one consistent visibility model for project/worktree thread rows so empty
|
||||
- no provider warnings.
|
||||
- Project/worktree error rows take precedence over provider warning and empty rows.
|
||||
- Clicking a `More` row must increase visible count for that path by +3.
|
||||
- Clicking a `More` row must persist that +3 increment to tree UI state for the same normalized path.
|
||||
- `ensureThreadVisible(path, provider, threadId)` must raise visible count in +3 steps until the target thread is visible and persist the increment.
|
||||
- Refresh bootstrap must restore persisted visible counts greater than default for known project/worktree paths.
|
||||
- Persisted visibility keys must use normalized paths so `/path` and `/path/` resolve to one entry.
|
||||
- Tree-side `More` click handling must not issue backend calls directly; it only updates visibility state.
|
||||
|
||||
[@test] ../sessions/testSrc/AgentSessionsToolWindowTest.kt
|
||||
[@test] ../sessions/testSrc/AgentSessionsServiceRefreshIntegrationTest.kt
|
||||
[@test] ../sessions/testSrc/AgentSessionsServiceOnDemandIntegrationTest.kt
|
||||
|
||||
## User Experience
|
||||
- Exact count case: `More (N)`.
|
||||
- Unknown count case: `More…`.
|
||||
- Empty row is muted helper text and mutually exclusive with `More` for the same node.
|
||||
- Non-default per-path thread visibility must persist across refresh/reopen for the same normalized path.
|
||||
|
||||
## Data & Backend
|
||||
- Unknown-count state is produced by the service aggregation layer (`hasUnknownThreadCount`), not by tree rendering logic.
|
||||
|
||||
@@ -14,7 +14,7 @@ targets:
|
||||
# Agent Threads Tool Window
|
||||
|
||||
Status: Draft
|
||||
Date: 2026-02-15
|
||||
Date: 2026-02-16
|
||||
|
||||
## Summary
|
||||
Define the Agent Threads tool window as a provider-agnostic, project-scoped session browser. Threads from supported providers are aggregated per project/worktree, rendered in one tree, and opened through a shared chat routing flow.
|
||||
@@ -39,6 +39,18 @@ Define the Agent Threads tool window as a provider-agnostic, project-scoped sess
|
||||
- If at least one provider succeeds, successful threads must be shown and failed providers must surface provider-local warning rows.
|
||||
- If all providers fail for a project/worktree load, show blocking project/worktree error state and suppress provider warning rows for that load.
|
||||
- Unknown provider totals must propagate via `hasUnknownThreadCount` and drive unknown-count `More…` rendering.
|
||||
- Sessions tree UI state must persist by normalized path:
|
||||
- collapsed project/worktree state,
|
||||
- per-path visible thread count,
|
||||
- open-path thread preview cache.
|
||||
- Refresh bootstrap must immediately seed open project/worktree nodes from cached previews when available and mark those paths loaded until live provider results arrive.
|
||||
- Refresh bootstrap must restore persisted visible thread counts above default for known project/worktree paths.
|
||||
- Refresh bootstrap must retain preview cache only for currently open project/worktree paths and prune stale closed-path entries.
|
||||
- Final merged refresh results must update preview cache for a path only when that path does not end in blocking error.
|
||||
- Auto-open default project expansion must skip paths persisted as collapsed.
|
||||
- User collapse/expand interactions must update persisted collapsed state.
|
||||
- Cached preview entries must preserve provider identity; missing legacy provider value must default to Codex for backward compatibility.
|
||||
- `More` and programmatic visibility expansion (`ensureThreadVisible`) must persist visible-count increments in tree UI state.
|
||||
- On-demand project/worktree loading must deduplicate concurrent requests for the same path.
|
||||
- Concurrent refresh requests must be deduplicated while a refresh is already running.
|
||||
- Project primary click must open/focus the project; closed projects must expose `Open` in context menu.
|
||||
@@ -56,6 +68,7 @@ Define the Agent Threads tool window as a provider-agnostic, project-scoped sess
|
||||
[@test] ../sessions/testSrc/AgentSessionsServiceOnDemandIntegrationTest.kt
|
||||
[@test] ../sessions/testSrc/AgentSessionsServiceConcurrencyIntegrationTest.kt
|
||||
[@test] ../sessions/testSrc/AgentSessionsToolWindowTest.kt
|
||||
[@test] ../sessions/testSrc/AgentSessionsTreeUiStateServiceTest.kt
|
||||
|
||||
## User Experience
|
||||
- Project rows are always expandable and may show worktree children.
|
||||
|
||||
Reference in New Issue
Block a user