IJPL-248623 split AWB task folders from Pi sessions

Task folders are Agent Workbench UI state, so move their persistence and Pi control handling out of the core Pi sessions module. Pi now exposes a generic control-request extension point, and the AWB plugin contributes task-folder handling through its own module and descriptor.

Keep Pi-owned integration details in Pi-owned artifacts: task-folder tools are separated from the generic bridge extension code, Pi settings strings live in the AWB Pi bundle, and provider references use the PI_AGENT_SESSION_PROVIDER constant instead of repeating the raw provider id.

This changelist also carries the session activity report model through providers, presentation code, tests, and specs so row activity and chrome activity can diverge without keeping task-folder or provider-specific state in core session actions.

GitOrigin-RevId: 3d35a81cb78599fb0115b0eb876f48ab58579924
This commit is contained in:
Vladimir Krivosheev
2026-07-09 18:59:26 +00:00
committed by intellij-monorepo-bot
parent 09ff8a47d5
commit 17519f0daa
114 changed files with 1158 additions and 826 deletions
+2
View File
@@ -81,6 +81,8 @@
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/sessions-jbcentral/intellij.agent.workbench.sessions.jbcentral.tests.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/sessions-jbcentral/intellij.agent.workbench.sessions.jbcentral.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/sessions-launch-config/backend/intellij.agent.workbench.sessions.launch.config.backend.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/sessions-launch-config/backend/intellij.agent.workbench.sessions.launch.config.backend.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/sessions-launch-config/backend/intellij.agent.workbench.sessions.launch.config.backend.tests.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/sessions-launch-config/backend/intellij.agent.workbench.sessions.launch.config.backend.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/sessions-task-folders/intellij.agent.workbench.sessions.task.folders.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/sessions-task-folders/intellij.agent.workbench.sessions.task.folders.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/sessions-task-folders/intellij.agent.workbench.sessions.task.folders.tests.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/sessions-task-folders/intellij.agent.workbench.sessions.task.folders.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/sessions/intellij.agent.workbench.sessions.tests.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/sessions/intellij.agent.workbench.sessions.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/sessions-toolwindow/intellij.agent.workbench.sessions.toolwindow.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/sessions-toolwindow/intellij.agent.workbench.sessions.toolwindow.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/sessions-toolwindow/intellij.agent.workbench.sessions.toolwindow.tests.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/sessions-toolwindow/intellij.agent.workbench.sessions.toolwindow.tests.iml" />
+1
View File
@@ -1014,6 +1014,7 @@ plugins/agent-workbench/sessions-actions
plugins/agent-workbench/sessions-cost
plugins/agent-workbench/sessions-jbcentral
plugins/agent-workbench/sessions-launch-config/backend
plugins/agent-workbench/sessions-task-folders
plugins/agent-workbench/sessions-toolwindow
plugins/agent-workbench/settings
plugins/agent-workbench/ui
@@ -103,7 +103,7 @@ internal class AgentChatVirtualFile internal constructor(
get() = resolveAgentChatThreadPresentation(this).title
override val threadActivity: AgentThreadActivity
get() = resolveAgentChatThreadPresentation(this).activity
get() = resolveAgentChatThreadPresentation(this).activityReport.rowActivity
var pendingCreatedAtMs: Long? = null
private set
@@ -783,7 +783,7 @@ class AgentChatEditorServiceTest {
val updatedTabs = publishThreadPresentation(
file = file,
title = "Renamed by source update",
activity = AgentThreadActivity.UNREAD,
activityReport = AgentThreadActivityReport(AgentThreadActivity.UNREAD),
)
assertThat(updatedTabs).isEqualTo(1)
@@ -794,7 +794,7 @@ class AgentChatEditorServiceTest {
val unchangedTabs = publishThreadPresentation(
file = file,
title = "Renamed by source update",
activity = AgentThreadActivity.UNREAD,
activityReport = AgentThreadActivityReport(AgentThreadActivity.UNREAD),
)
assertThat(unchangedTabs).isEqualTo(0)
}
@@ -873,7 +873,7 @@ class AgentChatEditorServiceTest {
file = file,
path = "${file.projectPath}/",
title = "Renamed by normalized source update",
activity = AgentThreadActivity.UNREAD,
activityReport = AgentThreadActivityReport(AgentThreadActivity.UNREAD),
)
assertThat(updatedTabs).isEqualTo(1)
@@ -885,7 +885,7 @@ class AgentChatEditorServiceTest {
file = file,
path = "${file.projectPath}/",
title = "Renamed by normalized source update",
activity = AgentThreadActivity.UNREAD,
activityReport = AgentThreadActivityReport(AgentThreadActivity.UNREAD),
)
assertThat(unchangedTabs).isEqualTo(0)
}
@@ -963,7 +963,7 @@ class AgentChatEditorServiceTest {
val updatedTabs = publishThreadPresentation(
file = openedChatFiles().first { it.subAgentId == null },
title = "Renamed parent",
activity = null,
activityReport = null,
)
assertThat(updatedTabs).isEqualTo(1)
@@ -992,7 +992,7 @@ class AgentChatEditorServiceTest {
val updatedTabs = publishThreadPresentation(
file = openedChatFiles().first { it.subAgentId == null },
title = "Renamed parent",
activity = AgentThreadActivity.UNREAD,
activityReport = AgentThreadActivityReport(AgentThreadActivity.UNREAD),
)
assertThat(updatedTabs).isEqualTo(2)
@@ -1099,7 +1099,7 @@ class AgentChatEditorServiceTest {
provider = AgentSessionProvider.from("codex"),
threadId = targetThreadId,
title = sharedTitle,
activity = AgentThreadActivity.PROCESSING,
activityReport = AgentThreadActivityReport(AgentThreadActivity.PROCESSING),
)).isEqualTo(0)
val rebindReport = rebindOpenPendingCodexTabs(
@@ -1479,7 +1479,7 @@ class AgentChatEditorServiceTest {
provider = AgentSessionProvider.from("codex"),
threadId = targetThreadId,
title = sharedTitle,
activity = AgentThreadActivity.PROCESSING,
activityReport = AgentThreadActivityReport(AgentThreadActivity.PROCESSING),
)).isEqualTo(0)
val rebindReport = rebindOpenConcreteCodexTabs(
@@ -1964,7 +1964,7 @@ class AgentChatEditorServiceTest {
provider = AgentSessionProvider.from("codex"),
threadId = "thread-1",
title = "Main thread",
activity = AgentThreadActivity.READY,
activityReport = AgentThreadActivityReport(AgentThreadActivity.READY),
)
assertThat(presentationModel.resolve(rootPresentationKey)).isNotNull
val matchingTabKeys = beforeCleanup
@@ -2024,7 +2024,7 @@ class AgentChatEditorServiceTest {
provider = AgentSessionProvider.from("codex"),
threadId = "thread-1",
title = "Main thread",
activity = AgentThreadActivity.READY,
activityReport = AgentThreadActivityReport(AgentThreadActivity.READY),
)
closeAndForgetAgentChatsForThread(
@@ -2544,23 +2544,7 @@ private suspend fun publishThreadPresentation(
file: AgentChatVirtualFile,
path: String = file.projectPath,
title: String,
activity: AgentThreadActivity?,
): Int {
val provider = checkNotNull(file.provider)
return publishThreadPresentation(
path = path,
provider = provider,
threadId = file.sessionId,
title = title,
activity = activity,
)
}
private suspend fun publishThreadPresentation(
file: AgentChatVirtualFile,
path: String = file.projectPath,
title: String,
activityReport: AgentThreadActivityReport,
activityReport: AgentThreadActivityReport?,
): Int {
val provider = checkNotNull(file.provider)
return publishThreadPresentation(
@@ -2577,31 +2561,13 @@ private suspend fun publishThreadPresentation(
provider: AgentSessionProvider,
threadId: String,
title: String,
activity: AgentThreadActivity?,
activityReport: AgentThreadActivityReport?,
): Int {
val changeSet = service<AgentSessionThreadPresentationModel>().updateThread(
path = path,
provider = provider,
threadId = threadId,
title = title,
activity = activity,
)
return AgentChatOpenTabPresentationInvalidator.invalidate(changeSet)
}
private suspend fun publishThreadPresentation(
path: String,
provider: AgentSessionProvider,
threadId: String,
title: String,
activityReport: AgentThreadActivityReport,
): Int {
val changeSet = service<AgentSessionThreadPresentationModel>().updateThread(
path = path,
provider = provider,
threadId = threadId,
title = title,
activity = null,
activityReport = activityReport,
)
return AgentChatOpenTabPresentationInvalidator.invalidate(changeSet)
@@ -2,6 +2,7 @@
package com.intellij.agent.workbench.chat
import com.intellij.platform.ai.agent.core.AgentThreadActivity
import com.intellij.platform.ai.agent.core.AgentThreadActivityReport
import com.intellij.platform.ai.agent.core.buildAgentThreadIdentity
import com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode
import com.intellij.platform.ai.agent.core.session.AgentSessionProvider
@@ -879,7 +880,7 @@ class AgentChatFileEditorLifecycleTest {
title = "Restored thread",
updatedAt = 1L,
archived = true,
activity = AgentThreadActivity.READY,
activityReport = AgentThreadActivityReport(AgentThreadActivity.READY),
provider = AgentSessionProvider.from("codex"),
)
),
@@ -1055,10 +1055,10 @@ class AgentChatFileEditorProviderTest {
val otherPathKey = presentationKey("/work/project-b", AgentSessionProvider.from("codex"), "thread-3")
model.replaceForTests(
mapOf(
refreshedKey to AgentSessionThreadPresentation(title = "Old title", activity = AgentThreadActivity.READY),
removedKey to AgentSessionThreadPresentation(title = "Removed title", activity = AgentThreadActivity.UNREAD),
otherProviderKey to AgentSessionThreadPresentation(title = "Claude title", activity = AgentThreadActivity.PROCESSING),
otherPathKey to AgentSessionThreadPresentation(title = "Other path", activity = AgentThreadActivity.READY),
refreshedKey to AgentSessionThreadPresentation(title = "Old title", activityReport = AgentThreadActivityReport(AgentThreadActivity.READY)),
removedKey to AgentSessionThreadPresentation(title = "Removed title", activityReport = AgentThreadActivityReport(AgentThreadActivity.UNREAD)),
otherProviderKey to AgentSessionThreadPresentation(title = "Claude title", activityReport = AgentThreadActivityReport(AgentThreadActivity.PROCESSING)),
otherPathKey to AgentSessionThreadPresentation(title = "Other path", activityReport = AgentThreadActivityReport(AgentThreadActivity.READY)),
)
)
@@ -1084,9 +1084,9 @@ class AgentChatFileEditorProviderTest {
)
assertThat(model.resolve(removedKey)).isNull()
assertThat(model.resolve(otherProviderKey))
.isEqualTo(AgentSessionThreadPresentation(title = "Claude title", activity = AgentThreadActivity.PROCESSING))
.isEqualTo(AgentSessionThreadPresentation(title = "Claude title", activityReport = AgentThreadActivityReport(AgentThreadActivity.PROCESSING)))
assertThat(model.resolve(otherPathKey))
.isEqualTo(AgentSessionThreadPresentation(title = "Other path", activity = AgentThreadActivity.READY))
.isEqualTo(AgentSessionThreadPresentation(title = "Other path", activityReport = AgentThreadActivityReport(AgentThreadActivity.READY)))
}
@Test
@@ -1099,7 +1099,7 @@ class AgentChatFileEditorProviderTest {
provider = AgentSessionProvider.from("codex"),
threadId = "thread-1",
title = "Existing title",
activity = AgentThreadActivity.READY,
activityReport = AgentThreadActivityReport(AgentThreadActivity.READY),
)
val changeSet = model.updateActivityHints(
@@ -1108,14 +1108,14 @@ class AgentChatFileEditorProviderTest {
com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadActivityPresentationUpdate(
path = "/work/project-a",
threadId = "thread-1",
activity = AgentThreadActivity.UNREAD,
activityReport = AgentThreadActivityReport(AgentThreadActivity.UNREAD),
)
),
)
assertThat(changeSet.changedKeys).containsExactly(key)
assertThat(model.resolve(key))
.isEqualTo(AgentSessionThreadPresentation(title = "Existing title", activity = AgentThreadActivity.UNREAD))
.isEqualTo(AgentSessionThreadPresentation(title = "Existing title", activityReport = AgentThreadActivityReport(AgentThreadActivity.UNREAD)))
}
}
@@ -1143,7 +1143,7 @@ class AgentChatFileEditorProviderTest {
assertThat(file).isNotNull
assertThat(model.resolve(key)).isNull()
assertThat(resolveAgentChatThreadPresentation(checkNotNull(file)))
.isEqualTo(AgentSessionThreadPresentation(title = "Restored thread", activity = AgentThreadActivity.UNREAD))
.isEqualTo(AgentSessionThreadPresentation(title = "Restored thread", activityReport = AgentThreadActivityReport(AgentThreadActivity.UNREAD)))
}
finally {
tabsService.forget(snapshot.tabKey)
@@ -1172,7 +1172,7 @@ class AgentChatFileEditorProviderTest {
provider = AgentSessionProvider.from("codex"),
threadId = snapshot.runtime.threadId,
title = "Forget me",
activity = AgentThreadActivity.UNREAD,
activityReport = AgentThreadActivityReport(AgentThreadActivity.UNREAD),
)
assertThat(model.resolve(key)).isNotNull
@@ -1475,7 +1475,7 @@ private fun threadModel(
title = title,
updatedAt = 1L,
archived = false,
activity = activity,
activityReport = AgentThreadActivityReport(activity),
provider = provider,
)
}
@@ -1,6 +1,7 @@
package com.intellij.agent.workbench.chat
import com.intellij.platform.ai.agent.core.AgentThreadActivity
import com.intellij.platform.ai.agent.core.AgentThreadActivityReport
import com.intellij.platform.ai.agent.core.buildAgentThreadIdentity
import com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode
import com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItem
@@ -933,7 +934,7 @@ private class OpenTabDispatchPiForkSource : AgentSessionSource, AgentSessionThre
title = "Forked Pi thread",
updatedAt = 5_000L,
archived = false,
activity = AgentThreadActivity.PROCESSING,
activityReport = AgentThreadActivityReport(AgentThreadActivity.PROCESSING),
provider = AgentSessionProvider.from("pi"),
),
launchSpecOverride = AgentSessionTerminalLaunchSpec(
@@ -210,7 +210,6 @@ f:com.intellij.platform.ai.agent.core.session.AgentSessionThread
- f:subAgents:java.util.List
- f:title:java.lang.String
- f:updatedAt:J
- b:<init>(java.lang.String,java.lang.String,J,Z,com.intellij.platform.ai.agent.core.AgentThreadActivity,java.lang.String,java.util.List,java.lang.String,com.intellij.platform.ai.agent.core.AgentThreadActivity,com.intellij.platform.ai.agent.core.session.AgentSessionCost,I,kotlin.jvm.internal.DefaultConstructorMarker):V
- b:<init>(java.lang.String,java.lang.String,J,Z,com.intellij.platform.ai.agent.core.AgentThreadActivityReport,java.lang.String,java.util.List,java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionCost,I,kotlin.jvm.internal.DefaultConstructorMarker):V
- f:component1():java.lang.String
- f:component2():java.lang.String
@@ -224,9 +223,7 @@ f:com.intellij.platform.ai.agent.core.session.AgentSessionThread
- f:copy-JxZNnm8(java.lang.String,java.lang.String,J,Z,com.intellij.platform.ai.agent.core.AgentThreadActivityReport,java.lang.String,java.util.List,java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionCost):com.intellij.platform.ai.agent.core.session.AgentSessionThread
- bs:copy-JxZNnm8$default(com.intellij.platform.ai.agent.core.session.AgentSessionThread,java.lang.String,java.lang.String,J,Z,com.intellij.platform.ai.agent.core.AgentThreadActivityReport,java.lang.String,java.util.List,java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionCost,I,java.lang.Object):com.intellij.platform.ai.agent.core.session.AgentSessionThread
- equals(java.lang.Object):Z
- f:getActivity():com.intellij.platform.ai.agent.core.AgentThreadActivity
- f:getProvider-dBzBBrw():java.lang.String
- f:getSummaryActivity():com.intellij.platform.ai.agent.core.AgentThreadActivity
- hashCode():I
f:com.intellij.platform.ai.agent.core.session.AgentSessionThreadOutline
- f:items:java.util.List
@@ -50,36 +50,7 @@ data class AgentSessionThread(
@JvmField val subAgents: List<AgentSubAgent> = emptyList(),
@JvmField val originBranch: String? = null,
@JvmField val cost: AgentSessionCost? = null,
) {
constructor(
id: String,
title: String,
updatedAt: Long,
archived: Boolean,
activity: AgentThreadActivity,
provider: AgentSessionProvider,
subAgents: List<AgentSubAgent> = emptyList(),
originBranch: String? = null,
summaryActivity: AgentThreadActivity? = activity,
cost: AgentSessionCost? = null,
) : this(
id = id,
title = title,
updatedAt = updatedAt,
archived = archived,
activityReport = AgentThreadActivityReport(rowActivity = activity, chromeActivity = summaryActivity),
provider = provider,
subAgents = subAgents,
originBranch = originBranch,
cost = cost,
)
val activity: AgentThreadActivity
get() = activityReport.rowActivity
val summaryActivity: AgentThreadActivity?
get() = activityReport.chromeActivity
}
)
/**
* Provider-neutral outline for a persisted agent thread.
@@ -277,7 +277,7 @@ internal class ClaudeSessionSource internal constructor(
title = sourcePrompt.threadTitle,
updatedAt = System.currentTimeMillis(),
archived = false,
activity = AgentThreadActivity.PROCESSING,
activityReport = AgentThreadActivityReport(AgentThreadActivity.PROCESSING),
provider = CLAUDE_AGENT_SESSION_PROVIDER,
),
launchSpecOverride = launchSpec,
@@ -347,10 +347,12 @@ private fun ClaudeBackendThread.toAgentSessionThread(
archived = archived,
provider = CLAUDE_AGENT_SESSION_PROVIDER,
originBranch = gitBranch,
activity = effectiveActivity(
readTracker = readTracker,
completedUnreadUpdatedAtByThreadId = completedUnreadUpdatedAtByThreadId,
observedUpdatedAtByThreadId = observedUpdatedAtByThreadId,
activityReport = AgentThreadActivityReport(
effectiveActivity(
readTracker = readTracker,
completedUnreadUpdatedAtByThreadId = completedUnreadUpdatedAtByThreadId,
observedUpdatedAtByThreadId = observedUpdatedAtByThreadId,
)
),
cost = if (includeCost) usageSnapshots.aggregateAgentSessionUsageCost(calculateCost) else null,
)
@@ -54,7 +54,7 @@ class ClaudeSessionSourceTest {
}
assertThat(result).hasSize(2)
assertThat(result).allMatch { it.activity == AgentThreadActivity.READY }
assertThat(result).allMatch { it.activityReport.rowActivity == AgentThreadActivity.READY }
}
@Test
@@ -105,12 +105,12 @@ class ClaudeSessionSourceTest {
val source = ClaudeSessionSource(backend = dynamicRefreshBackend { currentThreads })
runBlocking(Dispatchers.Default) {
assertThat(source.listThreads(path = "/any", openProject = null).single().activity)
assertThat(source.listThreads(path = "/any", openProject = null).single().activityReport.rowActivity)
.isEqualTo(AgentThreadActivity.READY)
// updatedAt increases but thread was never opened → stays READY.
currentThreads = listOf(ClaudeBackendThread(id = "s1", title = "Session 1", updatedAt = 2000L))
assertThat(source.listThreads(path = "/any", openProject = null).single().activity)
assertThat(source.listThreads(path = "/any", openProject = null).single().activityReport.rowActivity)
.isEqualTo(AgentThreadActivity.READY)
}
}
@@ -127,12 +127,12 @@ class ClaudeSessionSourceTest {
// User opens the thread.
source.markThreadAsRead("s1", 1000L)
assertThat(source.listThreads(path = "/any", openProject = null).single().activity)
assertThat(source.listThreads(path = "/any", openProject = null).single().activityReport.rowActivity)
.isEqualTo(AgentThreadActivity.READY)
// Agent replies while user is elsewhere → UNREAD.
currentThreads = listOf(ClaudeBackendThread(id = "s1", title = "Session 1", updatedAt = 2000L))
assertThat(source.listThreads(path = "/any", openProject = null).single().activity)
assertThat(source.listThreads(path = "/any", openProject = null).single().activityReport.rowActivity)
.isEqualTo(AgentThreadActivity.UNREAD)
}
}
@@ -150,12 +150,12 @@ class ClaudeSessionSourceTest {
// Agent replies → UNREAD.
currentThreads = listOf(ClaudeBackendThread(id = "s1", title = "Session 1", updatedAt = 2000L))
assertThat(source.listThreads(path = "/any", openProject = null).single().activity)
assertThat(source.listThreads(path = "/any", openProject = null).single().activityReport.rowActivity)
.isEqualTo(AgentThreadActivity.UNREAD)
// User returns → mark as read → READY.
source.markThreadAsRead("s1", 2000L)
assertThat(source.listThreads(path = "/any", openProject = null).single().activity)
assertThat(source.listThreads(path = "/any", openProject = null).single().activityReport.rowActivity)
.isEqualTo(AgentThreadActivity.READY)
}
}
@@ -175,7 +175,7 @@ class ClaudeSessionSourceTest {
currentThreads = listOf(
ClaudeBackendThread(id = "s1", title = "Session 1", updatedAt = 2000L, activity = ClaudeSessionActivity.PROCESSING),
)
assertThat(source.listThreads(path = "/any", openProject = null).single().activity)
assertThat(source.listThreads(path = "/any", openProject = null).single().activityReport.rowActivity)
.isEqualTo(AgentThreadActivity.PROCESSING)
}
}
@@ -195,7 +195,7 @@ class ClaudeSessionSourceTest {
currentThreads = listOf(
ClaudeBackendThread(id = "s1", title = "Session 1", updatedAt = 2000L, activity = ClaudeSessionActivity.NEEDS_INPUT),
)
assertThat(source.listThreads(path = "/any", openProject = null).single().activity)
assertThat(source.listThreads(path = "/any", openProject = null).single().activityReport.rowActivity)
.isEqualTo(AgentThreadActivity.NEEDS_INPUT)
}
}
@@ -217,7 +217,7 @@ class ClaudeSessionSourceTest {
source.markThreadAsRead("s1", 1000L)
currentThreads = listOf(ClaudeBackendThread(id = "s1", title = "Session 1", updatedAt = 2000L))
assertThat(source.listThreads(path = "/any", openProject = null).single().activity)
assertThat(source.listThreads(path = "/any", openProject = null).single().activityReport.rowActivity)
.isEqualTo(AgentThreadActivity.READY)
}
}
@@ -240,18 +240,18 @@ class ClaudeSessionSourceTest {
currentThreads = listOf(
ClaudeBackendThread(id = "s1", title = "Session 1", updatedAt = 2000L, activity = ClaudeSessionActivity.PROCESSING)
)
assertThat(source.refreshThreads(refreshRequest()).partialThreadsByPath.getValue("/any").single().activity)
assertThat(source.refreshThreads(refreshRequest()).partialThreadsByPath.getValue("/any").single().activityReport.rowActivity)
.isEqualTo(AgentThreadActivity.PROCESSING)
// Completion should still be surfaced as Done/UNREAD even if the thread remains selected.
currentThreads = listOf(ClaudeBackendThread(id = "s1", title = "Session 1", updatedAt = 3000L))
assertThat(source.refreshThreads(refreshRequest()).partialThreadsByPath.getValue("/any").single().activity)
assertThat(source.refreshThreads(refreshRequest()).partialThreadsByPath.getValue("/any").single().activityReport.rowActivity)
.isEqualTo(AgentThreadActivity.UNREAD)
// User switches away → clear active. A later update still becomes UNREAD.
source.setActiveThreadId(null)
currentThreads = listOf(ClaudeBackendThread(id = "s1", title = "Session 1", updatedAt = 4000L))
assertThat(source.refreshThreads(refreshRequest()).partialThreadsByPath.getValue("/any").single().activity)
assertThat(source.refreshThreads(refreshRequest()).partialThreadsByPath.getValue("/any").single().activityReport.rowActivity)
.isEqualTo(AgentThreadActivity.UNREAD)
}
}
@@ -264,18 +264,18 @@ class ClaudeSessionSourceTest {
val source = ClaudeSessionSource(backend = dynamicRefreshBackend { currentThreads })
runBlocking(Dispatchers.Default) {
assertThat(source.listThreads(path = "/any", openProject = null).single().activity)
assertThat(source.listThreads(path = "/any", openProject = null).single().activityReport.rowActivity)
.isEqualTo(AgentThreadActivity.READY)
currentThreads = listOf(ClaudeBackendThread(id = "s1", title = "Session 1", updatedAt = 2000L))
assertThat(source.refreshThreads(refreshRequest()).partialThreadsByPath.getValue("/any").single().activity)
assertThat(source.refreshThreads(refreshRequest()).partialThreadsByPath.getValue("/any").single().activityReport.rowActivity)
.isEqualTo(AgentThreadActivity.UNREAD)
assertThat(source.refreshThreads(refreshRequest()).partialThreadsByPath.getValue("/any").single().activity)
assertThat(source.refreshThreads(refreshRequest()).partialThreadsByPath.getValue("/any").single().activityReport.rowActivity)
.isEqualTo(AgentThreadActivity.UNREAD)
source.markThreadAsRead("s1", 2000L)
assertThat(source.refreshThreads(refreshRequest()).partialThreadsByPath.getValue("/any").single().activity)
assertThat(source.refreshThreads(refreshRequest()).partialThreadsByPath.getValue("/any").single().activityReport.rowActivity)
.isEqualTo(AgentThreadActivity.READY)
}
}
@@ -288,13 +288,13 @@ class ClaudeSessionSourceTest {
val source = ClaudeSessionSource(backend = dynamicRefreshBackend { currentThreads })
runBlocking(Dispatchers.Default) {
assertThat(source.listThreads(path = "/any", openProject = null).single().activity)
assertThat(source.listThreads(path = "/any", openProject = null).single().activityReport.rowActivity)
.isEqualTo(AgentThreadActivity.READY)
currentThreads = listOf(
ClaudeBackendThread(id = "s1", title = "Session 1", updatedAt = 2000L, awaitingAssistantTurn = true)
)
assertThat(source.refreshThreads(refreshRequest()).partialThreadsByPath.getValue("/any").single().activity)
assertThat(source.refreshThreads(refreshRequest()).partialThreadsByPath.getValue("/any").single().activityReport.rowActivity)
.isEqualTo(AgentThreadActivity.READY)
}
}
@@ -410,7 +410,7 @@ class ClaudeSessionSourceTest {
assertThat(result.map { it.id }).containsExactly("archived")
assertThat(result.single().archived).isTrue()
assertThat(result.single().activity).isEqualTo(AgentThreadActivity.PROCESSING)
assertThat(result.single().activityReport.rowActivity).isEqualTo(AgentThreadActivity.PROCESSING)
}
@Test
@@ -555,7 +555,7 @@ class ClaudeSessionSourceTest {
assertThat(UUID.fromString(forkResult.thread.id).toString()).isEqualTo(forkResult.thread.id)
assertThat(forkResult.thread.title).isEqualTo("Source Claude thread")
assertThat(forkResult.thread.provider).isEqualTo(AgentSessionProvider.from("claude"))
assertThat(forkResult.thread.activity).isEqualTo(AgentThreadActivity.PROCESSING)
assertThat(forkResult.thread.activityReport.rowActivity).isEqualTo(AgentThreadActivity.PROCESSING)
val launchSpec = requireNotNull(forkResult.launchSpecOverride)
assertThat(launchSpec.command).containsExactly(
"claude-test",
@@ -22,6 +22,7 @@ import com.intellij.platform.ai.agent.codex.sessions.backend.rollout.CodexRollou
import com.intellij.platform.ai.agent.codex.sessions.backend.toAgentSessionRefreshHints
import com.intellij.platform.ai.agent.codex.sessions.backend.toAgentThreadActivity
import com.intellij.platform.ai.agent.core.AgentThreadActivity
import com.intellij.platform.ai.agent.core.AgentThreadActivityReport
import com.intellij.platform.ai.agent.core.session.AgentSessionCost
import com.intellij.platform.ai.agent.core.session.AgentSessionCostKind
import com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItem
@@ -598,8 +599,10 @@ private fun toAgentSessionThread(
)
},
originBranch = thread.gitBranch,
activity = activity.toAgentThreadActivity(),
summaryActivity = summaryActivity?.toAgentThreadActivity(),
activityReport = AgentThreadActivityReport(
rowActivity = activity.toAgentThreadActivity(),
chromeActivity = summaryActivity?.toAgentThreadActivity(),
),
cost = cost,
)
}
@@ -72,7 +72,7 @@ class CodexSessionSourceRolloutIntegrationTest {
val listedThreads = source.listThreads(projectDir.toString(), openProject = null)
assertThat(listedThreads).hasSize(1)
assertThat(listedThreads.single().activity).isEqualTo(AgentThreadActivity.READY)
assertThat(listedThreads.single().activityReport.rowActivity).isEqualTo(AgentThreadActivity.READY)
}
}
@@ -236,7 +236,7 @@ class CodexSessionSourceRolloutIntegrationTest {
val listedThreads = source.listThreads(projectDir.toString(), openProject = null)
assertThat(listedThreads).hasSize(1)
assertThat(listedThreads.single().activity).isEqualTo(AgentThreadActivity.PROCESSING)
assertThat(listedThreads.single().activityReport.rowActivity).isEqualTo(AgentThreadActivity.PROCESSING)
}
}
@@ -296,7 +296,7 @@ class CodexSessionSourceRolloutIntegrationTest {
)
assertThat(refreshResult.completeThreadsByPath[projectPath]).hasSize(1)
assertThat(refreshResult.completeThreadsByPath.getValue(projectPath).single().activity).isEqualTo(AgentThreadActivity.READY)
assertThat(refreshResult.completeThreadsByPath.getValue(projectPath).single().activityReport.rowActivity).isEqualTo(AgentThreadActivity.READY)
}
}
@@ -323,7 +323,7 @@ class CodexSessionSourceRolloutIntegrationTest {
val listedThreads = source.listThreads(projectDir.toString(), openProject = null)
assertThat(listedThreads).hasSize(1)
assertThat(listedThreads.single().activity).isEqualTo(AgentThreadActivity.NEEDS_INPUT)
assertThat(listedThreads.single().activityReport.rowActivity).isEqualTo(AgentThreadActivity.NEEDS_INPUT)
}
}
@@ -354,7 +354,7 @@ class CodexSessionSourceRolloutIntegrationTest {
val listedThreads = source.listThreads(projectDir.toString(), openProject = null)
assertThat(listedThreads).hasSize(1)
assertThat(listedThreads.single().activity).isEqualTo(AgentThreadActivity.NEEDS_INPUT)
assertThat(listedThreads.single().activityReport.rowActivity).isEqualTo(AgentThreadActivity.NEEDS_INPUT)
}
}
@@ -1081,7 +1081,7 @@ class CodexSessionSourceTest {
assertThat(archivedThreads).hasSize(1)
assertThat(archivedThreads.single().id).isEqualTo("archived-1")
assertThat(archivedThreads.single().archived).isTrue()
assertThat(archivedThreads.single().activity).isEqualTo(AgentThreadActivity.READY)
assertThat(archivedThreads.single().activityReport.rowActivity).isEqualTo(AgentThreadActivity.READY)
}
}
@@ -1143,7 +1143,7 @@ class CodexSessionSourceTest {
val loadedCosts = source.loadThreadCosts(PROJECT_PATH, archivedThreads)
assertThat(archivedThreads).hasSize(1)
assertThat(archivedThreads.single().activity).isEqualTo(AgentThreadActivity.READY)
assertThat(archivedThreads.single().activityReport.rowActivity).isEqualTo(AgentThreadActivity.READY)
assertThat(archivedThreads.single().cost).isNull()
assertThat(loadedCosts.getValue(archivedThreadId)).isEqualTo(
AgentSessionCost(
@@ -1377,7 +1377,7 @@ class CodexSessionSourceTest {
assertThat(observedAppServerSeeds).isEmpty()
assertThat(observedRolloutSeeds).isEmpty()
assertThat(threads.single().activity).isEqualTo(AgentThreadActivity.PROCESSING)
assertThat(threads.single().activityReport.rowActivity).isEqualTo(AgentThreadActivity.PROCESSING)
}
}
@@ -1427,7 +1427,7 @@ class CodexSessionSourceTest {
assertThat(observedAppServerSeeds).isEmpty()
assertThat(observedRolloutSeeds).isEmpty()
assertThat(prefetched.getValue(PROJECT_PATH).single().activity).isEqualTo(AgentThreadActivity.READY)
assertThat(prefetched.getValue(PROJECT_PATH).single().activityReport.rowActivity).isEqualTo(AgentThreadActivity.READY)
}
}
}
@@ -5,6 +5,7 @@ import tools.jackson.core.JsonParser
import tools.jackson.core.JsonToken
import tools.jackson.core.json.JsonFactory
import com.intellij.platform.ai.agent.core.AgentThreadActivity
import com.intellij.platform.ai.agent.core.AgentThreadActivityReport
import com.intellij.platform.ai.agent.core.session.AgentSessionCost
import com.intellij.platform.ai.agent.core.normalizeAgentSessionProjectPath
import com.intellij.platform.ai.agent.core.normalizeAgentSessionTitle
@@ -339,7 +340,7 @@ internal class JunieSessionSource(
threadId = thread.id,
title = thread.title,
updatedAt = thread.updatedAt,
activity = thread.activity,
activity = thread.activityReport.rowActivity,
)
}
}
@@ -497,7 +498,7 @@ private fun JunieSessionIndexEntry.toAgentSessionThread(
title = title,
updatedAt = updatedAt,
archived = archived == true,
activity = effectiveActivity(readTracker, eventsAnalysis),
activityReport = AgentThreadActivityReport(effectiveActivity(readTracker, eventsAnalysis)),
provider = JUNIE_AGENT_SESSION_PROVIDER,
)
}
@@ -61,7 +61,7 @@ class JunieSessionSourceTest {
assertThat(threads[0].title).isEqualTo("Review changes")
assertThat(threads[0].updatedAt).isEqualTo(2000L)
assertThat(threads[0].archived).isFalse()
assertThat(threads[0].activity).isEqualTo(AgentThreadActivity.READY)
assertThat(threads[0].activityReport.rowActivity).isEqualTo(AgentThreadActivity.READY)
assertThat(threads[0].provider).isEqualTo(AgentSessionProvider.from("junie"))
}
}
@@ -100,7 +100,7 @@ class JunieSessionSourceTest {
assertThat(threads.map { it.id }).containsExactly("session-archived")
assertThat(threads.single().title).isEqualTo("Archived")
assertThat(threads.single().archived).isTrue()
assertThat(threads.single().activity).isEqualTo(AgentThreadActivity.READY)
assertThat(threads.single().activityReport.rowActivity).isEqualTo(AgentThreadActivity.READY)
assertThat(threads.single().provider).isEqualTo(AgentSessionProvider.from("junie"))
}
}
@@ -248,12 +248,12 @@ class JunieSessionSourceTest {
source.markThreadAsRead("session-read-state", 2000L)
val unreadThreads = source.listThreads(projectDir.toString(), openProject = null)
assertThat(unreadThreads.single().activity).isEqualTo(AgentThreadActivity.UNREAD)
assertThat(unreadThreads.single().activityReport.rowActivity).isEqualTo(AgentThreadActivity.UNREAD)
source.markThreadAsRead("session-read-state", 3000L)
val readyThreads = source.listThreads(projectDir.toString(), openProject = null)
assertThat(readyThreads.single().activity).isEqualTo(AgentThreadActivity.READY)
assertThat(readyThreads.single().activityReport.rowActivity).isEqualTo(AgentThreadActivity.READY)
}
}
@@ -342,15 +342,15 @@ class JunieSessionSourceTest {
val source = JunieSessionSource(sessionIndexPathProvider = { index })
val listedThreads = source.listThreads(projectDir.toString(), openProject = null)
assertThat(listedThreads.single().activity).isEqualTo(AgentThreadActivity.READY)
assertThat(listedThreads.single().activityReport.rowActivity).isEqualTo(AgentThreadActivity.READY)
val refreshResult = source.refreshThreads(
threadScopedRequest(projectDir, "session-running-status")
)
val refreshedThread = refreshResult.partialThreadsByPath.getValue(projectDir.toString()).single()
assertThat(refreshedThread.activity).isEqualTo(AgentThreadActivity.PROCESSING)
assertThat(source.listThreads(projectDir.toString(), openProject = null).single().activity).isEqualTo(AgentThreadActivity.PROCESSING)
assertThat(refreshedThread.activityReport.rowActivity).isEqualTo(AgentThreadActivity.PROCESSING)
assertThat(source.listThreads(projectDir.toString(), openProject = null).single().activityReport.rowActivity).isEqualTo(AgentThreadActivity.PROCESSING)
}
}
@@ -380,7 +380,7 @@ class JunieSessionSourceTest {
)
val refreshedThread = refreshResult.partialThreadsByPath.getValue(projectDir.toString()).single()
assertThat(refreshedThread.activity).isEqualTo(AgentThreadActivity.UNREAD)
assertThat(refreshedThread.activityReport.rowActivity).isEqualTo(AgentThreadActivity.UNREAD)
}
}
@@ -1,6 +1,7 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.ai.agent.opencode.sessions
import com.intellij.platform.ai.agent.core.AgentThreadActivityReport
import com.intellij.platform.ai.agent.core.normalizeAgentSessionProjectPath
import com.intellij.platform.ai.agent.core.normalizeAgentSessionTitle
import com.intellij.platform.ai.agent.core.session.AgentSessionThread
@@ -119,7 +120,7 @@ private fun OpenCodeSessionIndexEntry.toAgentSessionThread(readTracker: Map<Stri
title = title,
updatedAt = updatedAt,
archived = archived,
activity = resolveReadTrackedActivity(readTracker = readTracker, threadId = sessionId, updatedAt = updatedAt),
activityReport = AgentThreadActivityReport(resolveReadTrackedActivity(readTracker = readTracker, threadId = sessionId, updatedAt = updatedAt)),
provider = OPENCODE_AGENT_SESSION_PROVIDER,
)
}
@@ -80,7 +80,7 @@ class OpenCodeSessionSourceTest {
assertThat(threads.map { it.title }).containsExactly("New thread", "Worktree session", "Older thread")
assertThat(threads.map { it.provider }).containsOnly(AgentSessionProvider.from("opencode"))
assertThat(threads.map { it.archived }).containsOnly(false)
assertThat(threads.map { it.activity }).containsOnly(AgentThreadActivity.READY)
assertThat(threads.map { it.activityReport.rowActivity }).containsOnly(AgentThreadActivity.READY)
}
@Test
@@ -39,7 +39,6 @@ jvm_library(
"//plugins/agent-workbench/lib-agent/core",
"//plugins/agent-workbench/lib-agent/json",
"//plugins/agent-workbench/prompt/core",
"//plugins/agent-workbench/sessions",
"//plugins/agent-workbench/lib-agent/sessions-core:ai-agent-sessions-core",
"//libraries/jackson3/jackson:libraries-jackson3",
"//libraries/netty/buffer",
@@ -78,7 +77,6 @@ jvm_library(
"//plugins/agent-workbench/lib-agent/core:core_test_lib",
"//plugins/agent-workbench/lib-agent/json:json_test_lib",
"//plugins/agent-workbench/prompt/core:core_test_lib",
"//plugins/agent-workbench/sessions:sessions_test_lib",
"//plugins/agent-workbench/lib-agent/sessions-core:ai-agent-sessions-core_test_lib",
"//libraries/jackson3/jackson:libraries-jackson3_test_lib",
"//libraries/netty/buffer:buffer_test_lib",
@@ -175,6 +173,10 @@ jvm_library(
"//plugins/agent-workbench/prompt/core:core_test_lib",
"//plugins/agent-workbench/sessions",
"//plugins/agent-workbench/sessions:sessions_test_lib",
"//plugins/agent-workbench/sessions-task-folders:agent-workbench-sessions-task-folders",
"//plugins/agent-workbench/sessions-task-folders:agent-workbench-sessions-task-folders_test_lib",
"//plugins/agent-workbench/pi/awb",
"//plugins/agent-workbench/pi/awb:awb_test_lib",
"//plugins/agent-workbench/settings",
"//plugins/agent-workbench/settings:settings_test_lib",
"//plugins/agent-workbench/lib-agent/sessions-core:ai-agent-sessions-core",
@@ -6,8 +6,10 @@ The wire format is JSON with a string `type` field. Those strings are part of th
IDE-side routing is intentionally explicit:
- `PiExtensionControlBridge` owns the WebSocket lifecycle, authentication, connected-session registry, pending request correlation, and session update events.
- `PiExtensionControlProtocol` owns the typed message enum, payload parsing, and shared response builders.
- `PiTaskFolderControlHandler` owns task-folder and task-folder metadata requests backed by `AgentTaskFolderService`.
- `PiExtensionControlProtocol` owns the typed built-in message enum, payload parsing, extension request envelope, and shared response builders.
- `PiControlRequestHandler` extensions own domain-specific extension requests. The Agent Workbench Pi module registers
`PiTaskFolderControlHandler` for `taskFolderRequest` frames backed by `AgentTaskFolderService`.
Do not add reflection-based dispatch here. New message families should get a focused handler and be routed from `PiControlMessageType` by explicit `when` branches.
Do not add reflection-based dispatch here. New built-in Pi transport commands should be routed from `PiControlMessageType` by explicit `when` branches;
domain-specific Agent Workbench requests should get focused `PiControlRequestHandler` implementations.
This keeps PI transport concerns separate from task-folder domain state and avoids growing the bridge into a single control object.
@@ -26,7 +26,6 @@
<orderEntry type="module" module-name="intellij.platform.ai.agent.core" />
<orderEntry type="module" module-name="intellij.platform.ai.agent.json" />
<orderEntry type="module" module-name="intellij.agent.workbench.prompt.core" />
<orderEntry type="module" module-name="intellij.agent.workbench.sessions" />
<orderEntry type="module" module-name="intellij.platform.ai.agent.sessions.core" />
<orderEntry type="module" module-name="intellij.libraries.jackson3" />
<orderEntry type="module" module-name="intellij.libraries.netty.buffer" />
@@ -25,6 +25,8 @@
<orderEntry type="module" module-name="intellij.platform.ai.agent.json" scope="TEST" />
<orderEntry type="module" module-name="intellij.agent.workbench.prompt.core" scope="TEST" />
<orderEntry type="module" module-name="intellij.agent.workbench.sessions" scope="TEST" />
<orderEntry type="module" module-name="intellij.agent.workbench.sessions.task.folders" scope="TEST" />
<orderEntry type="module" module-name="intellij.agent.workbench.pi.awb" scope="TEST" />
<orderEntry type="module" module-name="intellij.agent.workbench.settings" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.ai.agent.sessions.core" scope="TEST" />
<orderEntry type="module" module-name="intellij.libraries.jackson3" scope="TEST" />
@@ -21,6 +21,10 @@
qualifiedName="com.intellij.agent.workbench.pi.jbCentralModelContributor"
interface="com.intellij.platform.ai.agent.pi.sessions.PiJbCentralModelContributor"
dynamic="true"/>
<extensionPoint
qualifiedName="com.intellij.agent.workbench.pi.controlRequestHandler"
interface="com.intellij.platform.ai.agent.pi.sessions.PiControlRequestHandler"
dynamic="true"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
@@ -1,5 +1,5 @@
import {type ExtensionAPI, type ExtensionContext} from "@earendil-works/pi-coding-agent";
import {registerTaskFolderTools, startControlBridge} from "./control.ts";
import {startControlBridge} from "./control.ts";
import {registerJbCentralProvider} from "./jbcentral.ts";
import {parseModelCatalogMetadata} from "./modelCatalog.ts";
import {registerOmlxProviders} from "./omlx.ts";
@@ -11,6 +11,7 @@ import {
resolveStartupActivity,
} from "./status.ts";
import {subscribeShiftEnterTerminalInput} from "./terminalInput.ts";
import {registerTaskFolderTools} from "./taskFolders.ts";
import {applyCurrentTheme, startStateWatcher} from "./theme.ts";
const MODEL_CATALOG_ENV = "AGENT_WORKBENCH_PI_MODEL_CATALOG";
@@ -1,5 +1,4 @@
import {Type} from "@earendil-works/pi-ai";
import {defineTool, type ExtensionAPI, type ExtensionContext} from "@earendil-works/pi-coding-agent";
import {type ExtensionContext} from "@earendil-works/pi-coding-agent";
import * as crypto from "node:crypto";
import * as http from "node:http";
import {type Duplex} from "node:stream";
@@ -11,9 +10,9 @@ const STATUS_TOKEN_ENV = "AGENT_WORKBENCH_PI_STATUS_TOKEN";
const CONTROL_ENDPOINT = process.env[CONTROL_ENDPOINT_ENV];
const STATUS_TOKEN = process.env[STATUS_TOKEN_ENV];
type AgentWorkbenchControlBridge = {
export type AgentWorkbenchControlBridge = {
setContext: (ctx: ExtensionContext) => void;
requestTaskFolder: <T = unknown>(operation: string, args?: Record<string, unknown>) => Promise<T>;
request: <T = unknown>(type: string, payload?: Record<string, unknown>) => Promise<T>;
close: () => void;
};
@@ -23,10 +22,9 @@ type AgentWorkbenchControlMessageType =
| "response"
| "navigateTree"
| "forkFromEntry"
| "taskFolderRequest";
| (string & {});
type AgentWorkbenchControlSessionMessageType = "hello" | "sessionState";
type AgentWorkbenchControlRequestType = "taskFolderRequest";
type AgentWorkbenchControlCommand = {
type?: AgentWorkbenchControlMessageType;
@@ -38,8 +36,6 @@ type AgentWorkbenchControlCommand = {
ok?: boolean;
cancelled?: boolean;
error?: string;
operation?: string;
arguments?: Record<string, unknown>;
result?: unknown;
};
@@ -50,24 +46,6 @@ type AgentWorkbenchControlThread = {
activity: string;
};
type AgentWorkbenchTaskFolder = {
path: string;
id: string;
name: string;
status: string;
metadata?: Record<string, string>;
createdAt?: number;
updatedAt?: number;
};
type AgentWorkbenchTaskFolderThread = {
path: string;
provider: string;
threadId: string;
folderId: string;
assignedAt?: number;
};
type AgentWorkbenchControlContext = ExtensionContext & {
navigateTree?: (entryId: string) => Promise<void> | void;
fork?: (
@@ -150,8 +128,8 @@ export function startControlBridge(ctx: ExtensionContext): AgentWorkbenchControl
};
const sendRequest = (
type: AgentWorkbenchControlRequestType,
payload: Partial<AgentWorkbenchControlCommand> = {},
type: string,
payload: Record<string, unknown> = {},
): Promise<AgentWorkbenchControlCommand> => {
const sessionId = currentCtx.sessionManager.getSessionId();
const cwd = currentCtx.cwd;
@@ -269,8 +247,8 @@ export function startControlBridge(ctx: ExtensionContext): AgentWorkbenchControl
currentCtx = nextCtx;
sendSessionMessage("sessionState");
},
requestTaskFolder: async <T = unknown>(operation: string, args: Record<string, unknown> = {}): Promise<T> => {
const response = requireOk(await sendRequest("taskFolderRequest", {operation, arguments: args}));
request: async <T = unknown>(type: string, payload: Record<string, unknown> = {}): Promise<T> => {
const response = requireOk(await sendRequest(type, payload));
return response.result as T;
},
close: () => {
@@ -284,255 +262,6 @@ export function startControlBridge(ctx: ExtensionContext): AgentWorkbenchControl
};
}
type AgentWorkbenchTaskFolderToolDefinition = {
name: string;
label: string;
description: string;
promptSnippet?: string;
promptGuidelines?: string[];
parameters: ReturnType<typeof Type.Object>;
operation: string;
arguments?: (params: Record<string, unknown>) => Record<string, unknown>;
confirm?: (params: Record<string, unknown>, ctx: ExtensionContext) => Promise<boolean>;
resultText: (result: unknown) => string;
};
export function registerTaskFolderTools(
pi: ExtensionAPI,
bridgeProvider: () => AgentWorkbenchControlBridge | undefined,
): void {
for (const definition of TASK_FOLDER_TOOL_DEFINITIONS) {
pi.registerTool(defineTool({
name: definition.name,
label: definition.label,
description: definition.description,
promptSnippet: definition.promptSnippet,
promptGuidelines: definition.promptGuidelines,
parameters: definition.parameters,
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const normalizedParams = params as Record<string, unknown>;
const bridge = bridgeProvider();
if (bridge === undefined) {
throw new Error("Agent Workbench control bridge is unavailable");
}
if (definition.confirm !== undefined && !(await definition.confirm(normalizedParams, ctx))) {
return {
content: [{type: "text", text: "Cancelled task folder operation."}],
details: {cancelled: true},
};
}
const result = await bridge.requestTaskFolder(definition.operation, definition.arguments?.(normalizedParams) ?? normalizedParams);
return {
content: [{type: "text", text: definition.resultText(result)}],
details: result,
};
},
}));
}
}
const TASK_FOLDER_METADATA_DESCRIPTION = "String metadata. Conventional keys are 'issue' and 'review'; custom keys are allowed.";
const TASK_FOLDER_TOOL_DEFINITIONS: AgentWorkbenchTaskFolderToolDefinition[] = [
{
name: "agent_workbench_get_current_task_folder",
label: "Get Current Task Folder",
description: "Get the Agent Workbench task folder assigned to the current Pi session, if any.",
promptSnippet: "Inspect the task folder assigned to the current Pi thread",
promptGuidelines: [
"Use agent_workbench_get_current_task_folder before updating a task folder when you are unsure whether this Pi thread is already assigned.",
],
parameters: Type.Object({}),
operation: "getCurrent",
resultText: (result) => {
const folder = resultFolder(result);
return folder === undefined ? "No task folder is assigned to this thread." : `Current task folder: '${folder.name}'.`;
},
},
{
name: "agent_workbench_list_task_folders",
label: "List Task Folders",
description: "List Agent Workbench task folders for the current project.",
promptSnippet: "List Agent Workbench task folders for the current project",
promptGuidelines: [
"Use agent_workbench_list_task_folders when you need a task folder id or need to choose an existing task folder.",
],
parameters: Type.Object({
includeDone: Type.Optional(Type.Boolean({description: "Whether to include done task folders"})),
}),
operation: "listFolders",
resultText: (result) => `Found ${resultFolders(result).length} task folder(s).`,
},
{
name: "agent_workbench_list_task_folder_threads",
label: "List Task Folder Threads",
description: "List threads assigned to an Agent Workbench task folder. Defaults to the current task folder.",
promptSnippet: "List threads assigned to an Agent Workbench task folder",
parameters: Type.Object({
folderId: Type.Optional(Type.String({description: "Task folder id; defaults to the current task folder"})),
}),
operation: "listThreads",
resultText: (result) => `Found ${resultThreads(result).length} assigned thread(s).`,
},
{
name: "agent_workbench_create_task_folder",
label: "Create Task Folder",
description: "Create an Agent Workbench task folder for the current project and assign the current Pi session to it.",
promptSnippet: "Create an Agent Workbench task folder and assign this Pi thread to it",
promptGuidelines: [
"Use agent_workbench_create_task_folder when the user asks to create a task folder or start work in a new task folder.",
"Use metadata key 'issue' for issue tracker ids and 'review' for review ids; do not use separate issue parameters.",
"If the current Pi thread already has a task folder, update it with metadata or rename tools instead of creating another folder.",
],
parameters: Type.Object({
name: Type.String({description: "Task folder name"}),
metadata: Type.Optional(Type.Record(Type.String(), Type.String({description: TASK_FOLDER_METADATA_DESCRIPTION}))),
}),
operation: "createAndAssign",
resultText: (result) => {
const folder = resultFolder(result);
if (folder === undefined) {
return "Task folder request completed.";
}
return resultBoolean(result, "created") === false
? `This thread is already assigned to task folder '${folder.name}'. Use metadata or rename tools for updates.`
: `Created task folder '${folder.name}' and assigned this thread.`;
},
},
{
name: "agent_workbench_assign_current_thread_to_task_folder",
label: "Assign Current Thread",
description: "Assign the current Pi session to an existing Agent Workbench task folder in the current project.",
promptSnippet: "Assign this Pi thread to an existing task folder",
parameters: Type.Object({
folderId: Type.String({description: "Task folder id"}),
}),
operation: "assignCurrentThread",
resultText: mutationResultText("Assigned current thread to task folder."),
},
{
name: "agent_workbench_remove_current_thread_from_task_folder",
label: "Remove Current Thread",
description: "Remove the current Pi session from its Agent Workbench task folder.",
promptSnippet: "Remove this Pi thread from its current task folder",
parameters: Type.Object({}),
operation: "unassignCurrentThread",
resultText: mutationResultText("Removed current thread from task folder."),
},
{
name: "agent_workbench_rename_task_folder",
label: "Rename Task Folder",
description: "Rename an Agent Workbench task folder. Defaults to the current task folder.",
promptSnippet: "Rename an Agent Workbench task folder",
parameters: Type.Object({
folderId: Type.Optional(Type.String({description: "Task folder id; defaults to the current task folder"})),
name: Type.String({description: "New task folder name"}),
}),
operation: "rename",
resultText: (result) => {
const folder = resultFolder(result);
return folder === undefined ? "Task folder rename completed." : `Renamed task folder to '${folder.name}'.`;
},
},
{
name: "agent_workbench_set_task_folder_metadata",
label: "Set Task Folder Metadata",
description: "Set a string metadata key on an Agent Workbench task folder. Defaults to the current task folder.",
promptSnippet: "Set task folder metadata such as issue or review",
promptGuidelines: [
"Use agent_workbench_set_task_folder_metadata to associate an issue id with an existing task folder using key 'issue'.",
"Use agent_workbench_set_task_folder_metadata to associate a review id with an existing task folder using key 'review'.",
],
parameters: Type.Object({
folderId: Type.Optional(Type.String({description: "Task folder id; defaults to the current task folder"})),
key: Type.String({description: "Metadata key. Conventional keys are 'issue' and 'review'."}),
value: Type.String({description: "Metadata value"}),
}),
operation: "setMetadata",
resultText: mutationResultText("Updated task folder metadata."),
},
{
name: "agent_workbench_delete_task_folder_metadata",
label: "Delete Task Folder Metadata",
description: "Delete a metadata key from an Agent Workbench task folder. Defaults to the current task folder.",
promptSnippet: "Delete task folder metadata",
parameters: Type.Object({
folderId: Type.Optional(Type.String({description: "Task folder id; defaults to the current task folder"})),
key: Type.String({description: "Metadata key to delete"}),
}),
operation: "deleteMetadata",
resultText: mutationResultText("Deleted task folder metadata."),
},
{
name: "agent_workbench_mark_task_folder_done",
label: "Mark Task Folder Done",
description: "Mark an Agent Workbench task folder done after archiving assigned threads. Defaults to the current task folder.",
promptSnippet: "Mark a task folder done and archive assigned threads",
parameters: Type.Object({
folderId: Type.Optional(Type.String({description: "Task folder id; defaults to the current task folder"})),
}),
operation: "markDone",
confirm: (_params, ctx) => confirmTaskFolderOperation(ctx, "Mark Task Folder Done", "Archive assigned threads and mark this task folder done?"),
resultText: (result) => {
const archived = resultNumber(result, "archivedCount");
const requested = resultNumber(result, "requestedCount");
return `Marked task folder done. Archived ${archived} of ${requested} assigned thread(s).`;
},
},
{
name: "agent_workbench_delete_task_folder",
label: "Delete Task Folder",
description: "Delete an Agent Workbench task folder and remove its assignments. Threads are not archived.",
parameters: Type.Object({
folderId: Type.Optional(Type.String({description: "Task folder id; defaults to the current task folder"})),
}),
operation: "delete",
confirm: (_params, ctx) => confirmTaskFolderOperation(ctx, "Delete Task Folder", "Delete this task folder and remove its thread assignments? Threads will not be archived."),
resultText: mutationResultText("Deleted task folder."),
},
];
async function confirmTaskFolderOperation(ctx: ExtensionContext, title: string, message: string): Promise<boolean> {
if (!ctx.hasUI) {
throw new Error("Task folder operation requires interactive confirmation");
}
return ctx.ui.confirm(title, message);
}
function mutationResultText(changedText: string): (result: unknown) => string {
return (result) => resultBoolean(result, "changed") ? changedText : "Task folder was already up to date.";
}
function resultFolder(result: unknown): AgentWorkbenchTaskFolder | undefined {
if (!isRecord(result)) return undefined;
const folder = result.folder;
return isRecord(folder) && typeof folder.name === "string" ? folder as AgentWorkbenchTaskFolder : undefined;
}
function resultFolders(result: unknown): AgentWorkbenchTaskFolder[] {
if (!isRecord(result) || !Array.isArray(result.folders)) return [];
return result.folders.filter(isRecord) as AgentWorkbenchTaskFolder[];
}
function resultThreads(result: unknown): AgentWorkbenchTaskFolderThread[] {
if (!isRecord(result) || !Array.isArray(result.threads)) return [];
return result.threads.filter(isRecord) as AgentWorkbenchTaskFolderThread[];
}
function resultBoolean(result: unknown, key: string): boolean | undefined {
if (!isRecord(result)) return undefined;
return typeof result[key] === "boolean" ? result[key] : undefined;
}
function resultNumber(result: unknown, key: string): number {
if (!isRecord(result)) return 0;
return typeof result[key] === "number" ? result[key] : 0;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function resolveCapabilities(ctx: ExtensionContext): { navigateTree: boolean; fork: boolean } {
const controlCtx = ctx as AgentWorkbenchControlContext;
return {
@@ -0,0 +1,273 @@
import {Type} from "@earendil-works/pi-ai";
import {defineTool, type ExtensionAPI, type ExtensionContext} from "@earendil-works/pi-coding-agent";
import {type AgentWorkbenchControlBridge} from "./control.ts";
const TASK_FOLDER_REQUEST_TYPE = "taskFolderRequest";
type AgentWorkbenchTaskFolder = {
path: string;
id: string;
name: string;
status: string;
metadata?: Record<string, string>;
createdAt?: number;
updatedAt?: number;
};
type AgentWorkbenchTaskFolderThread = {
path: string;
provider: string;
threadId: string;
folderId: string;
assignedAt?: number;
};
type AgentWorkbenchTaskFolderToolDefinition = {
name: string;
label: string;
description: string;
promptSnippet?: string;
promptGuidelines?: string[];
parameters: ReturnType<typeof Type.Object>;
operation: string;
arguments?: (params: Record<string, unknown>) => Record<string, unknown>;
confirm?: (params: Record<string, unknown>, ctx: ExtensionContext) => Promise<boolean>;
resultText: (result: unknown) => string;
};
export function registerTaskFolderTools(
pi: ExtensionAPI,
bridgeProvider: () => AgentWorkbenchControlBridge | undefined,
): void {
for (const definition of TASK_FOLDER_TOOL_DEFINITIONS) {
pi.registerTool(defineTool({
name: definition.name,
label: definition.label,
description: definition.description,
promptSnippet: definition.promptSnippet,
promptGuidelines: definition.promptGuidelines,
parameters: definition.parameters,
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const normalizedParams = params as Record<string, unknown>;
const bridge = bridgeProvider();
if (bridge === undefined) {
throw new Error("Agent Workbench control bridge is unavailable");
}
if (definition.confirm !== undefined && !(await definition.confirm(normalizedParams, ctx))) {
return {
content: [{type: "text", text: "Cancelled task folder operation."}],
details: {cancelled: true},
};
}
const args = definition.arguments?.(normalizedParams) ?? normalizedParams;
const result = await bridge.request(TASK_FOLDER_REQUEST_TYPE, {operation: definition.operation, arguments: args});
return {
content: [{type: "text", text: definition.resultText(result)}],
details: result,
};
},
}));
}
}
const TASK_FOLDER_METADATA_DESCRIPTION = "String metadata. Conventional keys are 'issue' and 'review'; custom keys are allowed.";
const TASK_FOLDER_TOOL_DEFINITIONS: AgentWorkbenchTaskFolderToolDefinition[] = [
{
name: "agent_workbench_get_current_task_folder",
label: "Get Current Task Folder",
description: "Get the Agent Workbench task folder assigned to the current Pi session, if any.",
promptSnippet: "Inspect the task folder assigned to the current Pi thread",
promptGuidelines: [
"Use agent_workbench_get_current_task_folder before updating a task folder when you are unsure whether this Pi thread is already assigned.",
],
parameters: Type.Object({}),
operation: "getCurrent",
resultText: (result) => {
const folder = resultFolder(result);
return folder === undefined ? "No task folder is assigned to this thread." : `Current task folder: '${folder.name}'.`;
},
},
{
name: "agent_workbench_list_task_folders",
label: "List Task Folders",
description: "List Agent Workbench task folders for the current project.",
promptSnippet: "List Agent Workbench task folders for the current project",
promptGuidelines: [
"Use agent_workbench_list_task_folders when you need a task folder id or need to choose an existing task folder.",
],
parameters: Type.Object({
includeDone: Type.Optional(Type.Boolean({description: "Whether to include done task folders"})),
}),
operation: "listFolders",
resultText: (result) => `Found ${resultFolders(result).length} task folder(s).`,
},
{
name: "agent_workbench_list_task_folder_threads",
label: "List Task Folder Threads",
description: "List threads assigned to an Agent Workbench task folder. Defaults to the current task folder.",
promptSnippet: "List threads assigned to an Agent Workbench task folder",
parameters: Type.Object({
folderId: Type.Optional(Type.String({description: "Task folder id; defaults to the current task folder"})),
}),
operation: "listThreads",
resultText: (result) => `Found ${resultThreads(result).length} assigned thread(s).`,
},
{
name: "agent_workbench_create_task_folder",
label: "Create Task Folder",
description: "Create an Agent Workbench task folder for the current project and assign the current Pi session to it.",
promptSnippet: "Create an Agent Workbench task folder and assign this Pi thread to it",
promptGuidelines: [
"Use agent_workbench_create_task_folder when the user asks to create a task folder or start work in a new task folder.",
"Use metadata key 'issue' for issue tracker ids and 'review' for review ids; do not use separate issue parameters.",
"If the current Pi thread already has a task folder, update it with metadata or rename tools instead of creating another folder.",
],
parameters: Type.Object({
name: Type.String({description: "Task folder name"}),
metadata: Type.Optional(Type.Record(Type.String(), Type.String({description: TASK_FOLDER_METADATA_DESCRIPTION}))),
}),
operation: "createAndAssign",
resultText: (result) => {
const folder = resultFolder(result);
if (folder === undefined) {
return "Task folder request completed.";
}
return resultBoolean(result, "created") === false
? `This thread is already assigned to task folder '${folder.name}'. Use metadata or rename tools for updates.`
: `Created task folder '${folder.name}' and assigned this thread.`;
},
},
{
name: "agent_workbench_assign_current_thread_to_task_folder",
label: "Assign Current Thread",
description: "Assign the current Pi session to an existing Agent Workbench task folder in the current project.",
promptSnippet: "Assign this Pi thread to an existing task folder",
parameters: Type.Object({
folderId: Type.String({description: "Task folder id"}),
}),
operation: "assignCurrentThread",
resultText: mutationResultText("Assigned current thread to task folder."),
},
{
name: "agent_workbench_remove_current_thread_from_task_folder",
label: "Remove Current Thread",
description: "Remove the current Pi session from its Agent Workbench task folder.",
promptSnippet: "Remove this Pi thread from its current task folder",
parameters: Type.Object({}),
operation: "unassignCurrentThread",
resultText: mutationResultText("Removed current thread from task folder."),
},
{
name: "agent_workbench_rename_task_folder",
label: "Rename Task Folder",
description: "Rename an Agent Workbench task folder. Defaults to the current task folder.",
promptSnippet: "Rename an Agent Workbench task folder",
parameters: Type.Object({
folderId: Type.Optional(Type.String({description: "Task folder id; defaults to the current task folder"})),
name: Type.String({description: "New task folder name"}),
}),
operation: "rename",
resultText: (result) => {
const folder = resultFolder(result);
return folder === undefined ? "Task folder rename completed." : `Renamed task folder to '${folder.name}'.`;
},
},
{
name: "agent_workbench_set_task_folder_metadata",
label: "Set Task Folder Metadata",
description: "Set a string metadata key on an Agent Workbench task folder. Defaults to the current task folder.",
promptSnippet: "Set task folder metadata such as issue or review",
promptGuidelines: [
"Use agent_workbench_set_task_folder_metadata to associate an issue id with an existing task folder using key 'issue'.",
"Use agent_workbench_set_task_folder_metadata to associate a review id with an existing task folder using key 'review'.",
],
parameters: Type.Object({
folderId: Type.Optional(Type.String({description: "Task folder id; defaults to the current task folder"})),
key: Type.String({description: "Metadata key. Conventional keys are 'issue' and 'review'."}),
value: Type.String({description: "Metadata value"}),
}),
operation: "setMetadata",
resultText: mutationResultText("Updated task folder metadata."),
},
{
name: "agent_workbench_delete_task_folder_metadata",
label: "Delete Task Folder Metadata",
description: "Delete a metadata key from an Agent Workbench task folder. Defaults to the current task folder.",
promptSnippet: "Delete task folder metadata",
parameters: Type.Object({
folderId: Type.Optional(Type.String({description: "Task folder id; defaults to the current task folder"})),
key: Type.String({description: "Metadata key to delete"}),
}),
operation: "deleteMetadata",
resultText: mutationResultText("Deleted task folder metadata."),
},
{
name: "agent_workbench_mark_task_folder_done",
label: "Mark Task Folder Done",
description: "Mark an Agent Workbench task folder done after archiving assigned threads. Defaults to the current task folder.",
promptSnippet: "Mark a task folder done and archive assigned threads",
parameters: Type.Object({
folderId: Type.Optional(Type.String({description: "Task folder id; defaults to the current task folder"})),
}),
operation: "markDone",
confirm: (_params, ctx) => confirmTaskFolderOperation(ctx, "Mark Task Folder Done", "Archive assigned threads and mark this task folder done?"),
resultText: (result) => {
const archived = resultNumber(result, "archivedCount");
const requested = resultNumber(result, "requestedCount");
return `Marked task folder done. Archived ${archived} of ${requested} assigned thread(s).`;
},
},
{
name: "agent_workbench_delete_task_folder",
label: "Delete Task Folder",
description: "Delete an Agent Workbench task folder and remove its assignments. Threads are not archived.",
parameters: Type.Object({
folderId: Type.Optional(Type.String({description: "Task folder id; defaults to the current task folder"})),
}),
operation: "delete",
confirm: (_params, ctx) => confirmTaskFolderOperation(ctx, "Delete Task Folder", "Delete this task folder and remove its thread assignments? Threads will not be archived."),
resultText: mutationResultText("Deleted task folder."),
},
];
async function confirmTaskFolderOperation(ctx: ExtensionContext, title: string, message: string): Promise<boolean> {
if (!ctx.hasUI) {
throw new Error("Task folder operation requires interactive confirmation");
}
return ctx.ui.confirm(title, message);
}
function mutationResultText(changedText: string): (result: unknown) => string {
return (result) => resultBoolean(result, "changed") ? changedText : "Task folder was already up to date.";
}
function resultFolder(result: unknown): AgentWorkbenchTaskFolder | undefined {
if (!isRecord(result)) return undefined;
const folder = result.folder;
return isRecord(folder) && typeof folder.name === "string" ? folder as AgentWorkbenchTaskFolder : undefined;
}
function resultFolders(result: unknown): AgentWorkbenchTaskFolder[] {
if (!isRecord(result) || !Array.isArray(result.folders)) return [];
return result.folders.filter(isRecord) as AgentWorkbenchTaskFolder[];
}
function resultThreads(result: unknown): AgentWorkbenchTaskFolderThread[] {
if (!isRecord(result) || !Array.isArray(result.threads)) return [];
return result.threads.filter(isRecord) as AgentWorkbenchTaskFolderThread[];
}
function resultBoolean(result: unknown, key: string): boolean | undefined {
if (!isRecord(result)) return undefined;
return typeof result[key] === "boolean" ? result[key] : undefined;
}
function resultNumber(result: unknown, key: string): number {
if (!isRecord(result)) return 0;
return typeof result[key] === "number" ? result[key] : 0;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
@@ -2,5 +2,7 @@
package com.intellij.platform.ai.agent.pi.sessions
import com.intellij.platform.ai.agent.core.session.AgentSessionProvider
import org.jetbrains.annotations.ApiStatus
internal val PI_AGENT_SESSION_PROVIDER: AgentSessionProvider = AgentSessionProvider.from("pi")
@ApiStatus.Internal
val PI_AGENT_SESSION_PROVIDER: AgentSessionProvider = AgentSessionProvider.from("pi")
@@ -0,0 +1,23 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.ai.agent.pi.sessions
import com.intellij.openapi.extensions.ExtensionPointName
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
interface PiControlRequestHandler {
val messageType: String
fun handle(
context: PiControlSessionContext,
request: PiControlExtensionRequest,
requestId: String,
sendResponse: (String) -> Unit,
)
companion object {
@JvmField
val EP_NAME: ExtensionPointName<PiControlRequestHandler> =
ExtensionPointName("com.intellij.agent.workbench.pi.controlRequestHandler")
}
}
@@ -30,7 +30,6 @@ private val CONTROL_LOG = logger<PiExtensionControlBridge>()
private val PI_CONTROL_CONNECTION_KEY: Key<PiControlConnection> = Key.create("agent.workbench.pi.control.connection")
internal object PiExtensionControlBridge {
private val taskFolderControlHandler = PiTaskFolderControlHandler()
private val connectionsBySessionId = ConcurrentHashMap<String, PiControlConnection>()
private val controlUpdates = MutableSharedFlow<AgentSessionSourceUpdateEvent>(
extraBufferCapacity = 64,
@@ -87,11 +86,12 @@ internal object PiExtensionControlBridge {
PiControlMessageType.HELLO -> handleHello(webSocketClient, payload)
PiControlMessageType.SESSION_STATE -> handleSessionState(webSocketClient, payload)
PiControlMessageType.RESPONSE -> handleResponse(webSocketClient, payload)
PiControlMessageType.TASK_FOLDER_REQUEST -> handleTaskFolderRequest(webSocketClient, payload)
PiControlMessageType.NAVIGATE_TREE,
PiControlMessageType.FORK_FROM_ENTRY,
null,
-> sendProtocolError(webSocketClient, requestId = payload.requestId, error = "Unsupported control message type")
null -> if (!handleExtensionRequest(webSocketClient, payload)) {
sendProtocolError(webSocketClient, requestId = payload.requestId, error = "Unsupported control message type")
}
}
}
@@ -171,20 +171,24 @@ internal object PiExtensionControlBridge {
)
}
private fun handleTaskFolderRequest(client: WebSocketClient, payload: PiControlPayload) {
private fun handleExtensionRequest(client: WebSocketClient, payload: PiControlPayload): Boolean {
val messageType = payload.typeName?.trim()?.takeIf { it.isNotEmpty() } ?: return false
val handler = PiControlRequestHandler.EP_NAME.extensionList.firstOrNull { candidate -> candidate.messageType == messageType }
?: return false
val connection = client.getUserData(PI_CONTROL_CONNECTION_KEY)
val requestId = payload.requestId?.trim()?.takeIf { it.isNotEmpty() }
if (connection == null || requestId == null) {
sendProtocolError(client, requestId = requestId, error = "Task folder request requires an authenticated control connection")
return
sendProtocolError(client, requestId = requestId, error = "Control request requires an authenticated control connection")
return true
}
taskFolderControlHandler.handle(
handler.handle(
context = PiControlSessionContext(projectPath = connection.projectPath, sessionId = connection.sessionId),
payload = payload,
request = PiControlExtensionRequest(operation = payload.operation, arguments = payload.arguments),
requestId = requestId,
sendResponse = { response -> sendControlText(client = client, text = response) },
)
return true
}
private suspend fun sendCommand(path: String, threadId: String, itemId: String, type: PiControlMessageType): PiControlResponse? {
@@ -2,6 +2,7 @@
package com.intellij.platform.ai.agent.pi.sessions
import com.intellij.platform.ai.agent.core.AgentThreadActivity
import com.intellij.platform.ai.agent.core.AgentThreadActivityReport
import com.intellij.platform.ai.agent.core.session.AgentSessionThread
import com.intellij.platform.ai.agent.json.createJsonGenerator
import com.intellij.platform.ai.agent.json.createJsonParser
@@ -11,6 +12,7 @@ import com.intellij.platform.ai.agent.json.readJsonStringOrNull
import tools.jackson.core.JsonParser
import tools.jackson.core.JsonToken
import tools.jackson.core.json.JsonFactory
import org.jetbrains.annotations.ApiStatus
import java.io.StringWriter
internal enum class PiControlMessageType(@JvmField val wireName: String) {
@@ -18,8 +20,7 @@ internal enum class PiControlMessageType(@JvmField val wireName: String) {
SESSION_STATE("sessionState"),
RESPONSE("response"),
NAVIGATE_TREE("navigateTree"),
FORK_FROM_ENTRY("forkFromEntry"),
TASK_FOLDER_REQUEST("taskFolderRequest");
FORK_FROM_ENTRY("forkFromEntry");
companion object {
private val byWireName: Map<String, PiControlMessageType> = entries.associateBy(PiControlMessageType::wireName)
@@ -30,11 +31,18 @@ internal enum class PiControlMessageType(@JvmField val wireName: String) {
}
}
internal data class PiControlSessionContext(
@ApiStatus.Internal
data class PiControlSessionContext(
@JvmField val projectPath: String,
@JvmField val sessionId: String,
)
@ApiStatus.Internal
data class PiControlExtensionRequest(
@JvmField val operation: String? = null,
@JvmField val arguments: PiControlRequestArguments? = null,
)
internal data class PiControlCapabilities(
@JvmField val navigateTree: Boolean,
@JvmField val fork: Boolean,
@@ -55,6 +63,7 @@ internal data class PiControlResponse(
internal data class PiControlPayload(
@JvmField val type: PiControlMessageType? = null,
@JvmField val typeName: String? = null,
@JvmField val requestId: String? = null,
@JvmField val token: String? = null,
@JvmField val sessionId: String? = null,
@@ -65,10 +74,11 @@ internal data class PiControlPayload(
@JvmField val thread: PiControlThreadPayload? = null,
@JvmField val capabilities: PiControlCapabilities? = null,
@JvmField val operation: String? = null,
@JvmField val arguments: PiTaskFolderControlArguments? = null,
@JvmField val arguments: PiControlRequestArguments? = null,
)
internal data class PiTaskFolderControlArguments(
@ApiStatus.Internal
data class PiControlRequestArguments(
@JvmField val folderId: String? = null,
@JvmField val name: String? = null,
@JvmField val key: String? = null,
@@ -90,7 +100,7 @@ internal data class PiControlThreadPayload(
title = title?.trim()?.takeIf { it.isNotEmpty() } ?: threadId,
updatedAt = updatedAt ?: System.currentTimeMillis(),
archived = false,
activity = activity ?: AgentThreadActivity.READY,
activityReport = AgentThreadActivityReport(activity ?: AgentThreadActivity.READY),
provider = PI_AGENT_SESSION_PROVIDER,
)
}
@@ -129,7 +139,8 @@ internal fun buildPiControlHelloAcknowledgement(requestId: String?, sessionId: S
}
}
internal fun buildPiControlErrorResponse(requestId: String?, error: String): String {
@ApiStatus.Internal
fun buildPiControlErrorResponse(requestId: String?, error: String): String {
return buildPiControlJsonObject { generator ->
generator.writeStringProperty("type", PiControlMessageType.RESPONSE.wireName)
requestId?.let { generator.writeStringProperty("requestId", it) }
@@ -138,6 +149,19 @@ internal fun buildPiControlErrorResponse(requestId: String?, error: String): Str
}
}
@ApiStatus.Internal
fun buildPiControlResultResponse(requestId: String, writeResult: (tools.jackson.core.JsonGenerator) -> Unit): String {
return buildPiControlJsonObject { generator ->
generator.writeStringProperty("type", PiControlMessageType.RESPONSE.wireName)
generator.writeStringProperty("requestId", requestId)
generator.writeBooleanProperty("ok", true)
generator.writeName("result")
generator.writeStartObject()
writeResult(generator)
generator.writeEndObject()
}
}
internal fun buildPiControlJsonObject(builder: (tools.jackson.core.JsonGenerator) -> Unit): String {
val writer = StringWriter()
PI_CONTROL_JSON_FACTORY.createJsonGenerator(writer).use { generator ->
@@ -150,6 +174,7 @@ internal fun buildPiControlJsonObject(builder: (tools.jackson.core.JsonGenerator
private fun readControlPayload(parser: JsonParser): PiControlPayload {
var type: PiControlMessageType? = null
var typeName: String? = null
var requestId: String? = null
var token: String? = null
var sessionId: String? = null
@@ -160,10 +185,14 @@ private fun readControlPayload(parser: JsonParser): PiControlPayload {
var thread: PiControlThreadPayload? = null
var capabilities: PiControlCapabilities? = null
var operation: String? = null
var arguments: PiTaskFolderControlArguments? = null
var arguments: PiControlRequestArguments? = null
forEachJsonObjectField(parser) { fieldName ->
when (fieldName) {
"type" -> type = PiControlMessageType.fromWireName(readJsonStringOrNull(parser))
"type" -> {
val wireName = readJsonStringOrNull(parser)
typeName = wireName
type = PiControlMessageType.fromWireName(wireName)
}
"requestId" -> requestId = readJsonStringOrNull(parser)
"token" -> token = readJsonStringOrNull(parser)
"sessionId" -> sessionId = readJsonStringOrNull(parser)
@@ -174,13 +203,14 @@ private fun readControlPayload(parser: JsonParser): PiControlPayload {
"thread" -> thread = readControlThreadPayload(parser)
"capabilities" -> capabilities = readControlCapabilities(parser)
"operation" -> operation = readJsonStringOrNull(parser)
"arguments" -> arguments = readTaskFolderControlArguments(parser)
"arguments" -> arguments = readControlRequestArguments(parser)
else -> parser.skipChildren()
}
true
}
return PiControlPayload(
type = type,
typeName = typeName,
requestId = requestId,
token = token,
sessionId = sessionId,
@@ -195,7 +225,7 @@ private fun readControlPayload(parser: JsonParser): PiControlPayload {
)
}
private fun readTaskFolderControlArguments(parser: JsonParser): PiTaskFolderControlArguments? {
private fun readControlRequestArguments(parser: JsonParser): PiControlRequestArguments? {
if (parser.currentToken() != JsonToken.START_OBJECT) {
parser.skipChildren()
return null
@@ -218,7 +248,7 @@ private fun readTaskFolderControlArguments(parser: JsonParser): PiTaskFolderCont
}
true
}
return PiTaskFolderControlArguments(
return PiControlRequestArguments(
folderId = folderId,
name = name,
key = key,
@@ -8,6 +8,7 @@ import tools.jackson.core.JsonParser
import tools.jackson.core.JsonToken
import tools.jackson.core.json.JsonFactory
import com.intellij.platform.ai.agent.core.AgentThreadActivity
import com.intellij.platform.ai.agent.core.AgentThreadActivityReport
import com.intellij.platform.ai.agent.core.normalizeAgentSessionProjectPath
import com.intellij.platform.ai.agent.core.normalizeAgentSessionTitle
import com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind
@@ -1431,7 +1432,7 @@ private fun PiSessionIndexEntry.toAgentSessionThread(readTracker: Map<String, Lo
title = title,
updatedAt = updatedAt,
archived = archived,
activity = activity ?: resolveCompletedPiActivity(readTracker = readTracker),
activityReport = AgentThreadActivityReport(activity ?: resolveCompletedPiActivity(readTracker = readTracker)),
provider = PI_AGENT_SESSION_PROVIDER,
)
}
@@ -1446,10 +1447,12 @@ private fun PiSessionIndexEntry.toAgentSessionThread(
title = title,
updatedAt = updatedAt,
archived = archived,
activity = activity ?: resolveCompletedPiActivity(
readTracker = readTracker,
completedUnreadUpdatedAtByThreadId = completedUnreadUpdatedAtByThreadId,
observedUpdatedAtByThreadId = observedUpdatedAtByThreadId,
activityReport = AgentThreadActivityReport(
activity ?: resolveCompletedPiActivity(
readTracker = readTracker,
completedUnreadUpdatedAtByThreadId = completedUnreadUpdatedAtByThreadId,
observedUpdatedAtByThreadId = observedUpdatedAtByThreadId,
)
),
provider = PI_AGENT_SESSION_PROVIDER,
)
@@ -325,6 +325,7 @@ private val PI_EXTENSION_RESOURCE_FILE_NAMES: List<String> = listOf(
"omlx.ts",
"status.ts",
"terminalInput.ts",
"taskFolders.ts",
"theme.ts",
)
private const val PI_EXTENSION_FILE_NAME_HASH_PREFIX_LENGTH: Int = 16
@@ -51,7 +51,7 @@ class PiAgentSessionProviderDescriptorTest {
@Test
fun exposesPiProviderMetadata() {
assertThat(PI_AGENT_SESSION_PROVIDER).isEqualTo(AgentSessionProvider.from("pi"))
assertThat(descriptor.sessionSource.provider).isEqualTo(PI_AGENT_SESSION_PROVIDER)
assertThat(descriptor.displayPriority).isEqualTo(3)
assertThat(descriptor.displayNameKey).isEqualTo("toolwindow.provider.pi")
assertThat(descriptor.newSessionLabelKey).isEqualTo("toolwindow.action.new.session.pi")
@@ -1063,7 +1063,7 @@ private fun knownSelection(provider: String, modelId: String, reasoning: Boolean
private fun emptySource(): AgentSessionSource {
return object : AgentSessionSource {
override val provider: AgentSessionProvider
get() = AgentSessionProvider.from("pi")
get() = PI_AGENT_SESSION_PROVIDER
override suspend fun listThreads(path: String, openProject: Project?): List<AgentSessionThread> = emptyList()
@@ -5,8 +5,8 @@ import com.intellij.platform.ai.agent.core.AgentThreadActivity
import com.intellij.platform.ai.agent.json.createJsonParser
import com.intellij.platform.ai.agent.json.forEachJsonObjectField
import com.intellij.platform.ai.agent.json.readJsonStringOrNull
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderService
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderStatus
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolderService
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolderStatus
import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionSourceUpdate
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.service
@@ -116,7 +116,7 @@ class PiExtensionControlWebSocketHandlerTest {
assertThat(forkedThread?.id).isEqualTo(forkedSessionId)
assertThat(forkedThread?.title).isEqualTo("Forked thread")
assertThat(forkedThread?.updatedAt).isEqualTo(9_000L)
assertThat(forkedThread?.activity).isEqualTo(AgentThreadActivity.PROCESSING)
assertThat(forkedThread?.activityReport?.rowActivity).isEqualTo(AgentThreadActivity.PROCESSING)
assertThat(PiExtensionControlBridge.canNavigateThreadOutlineItem(projectDir.toString(), sessionId, "entry-next")).isFalse()
assertThat(PiExtensionControlBridge.canNavigateThreadOutlineItem(projectDir.toString(), forkedSessionId, "entry-next")).isTrue()
assertThat(PiExtensionStatusBridge.authenticateLaunchToken(launchEnvironment.token, sessionId)).isNull()
@@ -402,6 +402,7 @@ class PiExtensionControlWebSocketHandlerTest {
}
private fun registerControlHandler(disposable: Disposable) {
ExtensionTestUtil.maskExtensions(PiControlRequestHandler.EP_NAME, listOf(PiTaskFolderControlHandler()), disposable)
ExtensionTestUtil.maskExtensions(HttpRequestHandler.EP_NAME, listOf(PiExtensionControlWebSocketHandler()), disposable)
}
@@ -6,7 +6,6 @@ import com.intellij.platform.ai.agent.core.AgentThreadActivityReport
import com.intellij.platform.ai.agent.core.session.AgentSessionCost
import com.intellij.platform.ai.agent.core.session.AgentSessionCostKind
import com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind
import com.intellij.platform.ai.agent.core.session.AgentSessionProvider
import com.intellij.platform.ai.agent.sessions.core.cost.AgentSessionUsageSnapshot
import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionActivityEvidence
import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionSourceUpdate
@@ -71,8 +70,8 @@ class PiSessionSourceTest {
assertThat(threads[0].title).isEqualTo("Named Pi session")
assertThat(threads[0].updatedAt).isEqualTo(3_000L)
assertThat(threads[0].archived).isFalse()
assertThat(threads[0].activity).isEqualTo(AgentThreadActivity.READY)
assertThat(threads[0].provider).isEqualTo(AgentSessionProvider.from("pi"))
assertThat(threads[0].activityReport.rowActivity).isEqualTo(AgentThreadActivity.READY)
assertThat(threads[0].provider).isEqualTo(PI_AGENT_SESSION_PROVIDER)
assertThat(threads[1].title).isEqualTo("Old task")
}
}
@@ -467,7 +466,7 @@ class PiSessionSourceTest {
val outline = source.loadThreadOutline(projectDir.toString(), "session-outline", null)
assertThat(outline).isNotNull
assertThat(outline!!.provider).isEqualTo(AgentSessionProvider.from("pi"))
assertThat(outline!!.provider).isEqualTo(PI_AGENT_SESSION_PROVIDER)
assertThat(outline.threadId).isEqualTo("session-outline")
assertThat(outline.title).isEqualTo("Named outline")
assertThat(outline.updatedAt).isEqualTo(3_000L)
@@ -524,7 +523,7 @@ class PiSessionSourceTest {
)
val forkedThread = checkNotNull(forkResult?.thread)
assertThat(forkedThread.provider).isEqualTo(AgentSessionProvider.from("pi"))
assertThat(forkedThread.provider).isEqualTo(PI_AGENT_SESSION_PROVIDER)
assertThat(forkedThread.id).isNotEqualTo("session-local-fork")
assertThat(forkedThread.title).isEqualTo("First task")
val forkedOutline = source.loadThreadOutline(projectDir.toString(), forkedThread.id, null)
@@ -617,15 +616,15 @@ class PiSessionSourceTest {
val store = PiSessionStore(sessionDirResolver = { sessionDir }, timeProvider = { now })
val source = PiSessionSource(sessionStore = store)
assertThat(source.listThreads(projectDir.toString(), openProject = null).single().activity).isEqualTo(AgentThreadActivity.PROCESSING)
assertThat(source.listThreads(projectDir.toString(), openProject = null).single().activityReport.rowActivity).isEqualTo(AgentThreadActivity.PROCESSING)
assertThat(store.archiveThread(projectDir.toString(), "session-archive-processing")).isTrue()
val archivedThread = source.listArchivedThreads(projectDir.toString(), openProject = null).single()
assertThat(archivedThread.activity).isEqualTo(AgentThreadActivity.PROCESSING)
assertThat(archivedThread.activityReport.rowActivity).isEqualTo(AgentThreadActivity.PROCESSING)
now = 5_000L
assertThat(store.unarchiveThread(projectDir.toString(), "session-archive-processing")).isTrue()
val activeThread = source.listThreads(projectDir.toString(), openProject = null).single()
assertThat(activeThread.activity).isEqualTo(AgentThreadActivity.PROCESSING)
assertThat(activeThread.activityReport.rowActivity).isEqualTo(AgentThreadActivity.PROCESSING)
}
}
@@ -693,10 +692,10 @@ class PiSessionSourceTest {
val source = sourceFor(sessionDir)
source.markThreadAsRead("session-read", 2_000L)
assertThat(source.listThreads(projectDir.toString(), openProject = null).single().activity).isEqualTo(AgentThreadActivity.UNREAD)
assertThat(source.listThreads(projectDir.toString(), openProject = null).single().activityReport.rowActivity).isEqualTo(AgentThreadActivity.UNREAD)
source.markThreadAsRead("session-read", 3_000L)
assertThat(source.listThreads(projectDir.toString(), openProject = null).single().activity).isEqualTo(AgentThreadActivity.READY)
assertThat(source.listThreads(projectDir.toString(), openProject = null).single().activityReport.rowActivity).isEqualTo(AgentThreadActivity.READY)
}
}
@@ -715,7 +714,7 @@ class PiSessionSourceTest {
val thread = source.listThreads(projectDir.toString(), openProject = null).single()
assertThat(thread.activity).isEqualTo(AgentThreadActivity.PROCESSING)
assertThat(thread.activityReport.rowActivity).isEqualTo(AgentThreadActivity.PROCESSING)
}
}
@@ -740,7 +739,7 @@ class PiSessionSourceTest {
val thread = source.listThreads(projectDir.toString(), openProject = null).single()
assertThat(thread.activity).isEqualTo(AgentThreadActivity.PROCESSING)
assertThat(thread.activityReport.rowActivity).isEqualTo(AgentThreadActivity.PROCESSING)
}
}
@@ -760,7 +759,7 @@ class PiSessionSourceTest {
val thread = source.listThreads(projectDir.toString(), openProject = null).single()
assertThat(thread.activity).isEqualTo(AgentThreadActivity.PROCESSING)
assertThat(thread.activityReport.rowActivity).isEqualTo(AgentThreadActivity.PROCESSING)
}
}
@@ -780,7 +779,7 @@ class PiSessionSourceTest {
val thread = source.listThreads(projectDir.toString(), openProject = null).single()
assertThat(thread.activity).isEqualTo(AgentThreadActivity.PROCESSING)
assertThat(thread.activityReport.rowActivity).isEqualTo(AgentThreadActivity.PROCESSING)
}
}
@@ -798,17 +797,17 @@ class PiSessionSourceTest {
val source = sourceFor(sessionDir)
val workingThread = source.listThreads(projectDir.toString(), openProject = null).single()
assertThat(workingThread.activity).isEqualTo(AgentThreadActivity.PROCESSING)
assertThat(workingThread.activityReport.rowActivity).isEqualTo(AgentThreadActivity.PROCESSING)
appendPiSessionEntry(
sessionFile,
piAssistantMessageEntry(id = "assistant-completed-observed", parentId = "user-completed-observed", timestamp = 4_000L),
)
val completedThread = source.listThreads(projectDir.toString(), openProject = null).single()
assertThat(completedThread.activity).isEqualTo(AgentThreadActivity.UNREAD)
assertThat(completedThread.activityReport.rowActivity).isEqualTo(AgentThreadActivity.UNREAD)
source.markThreadAsRead("session-completed-observed", 4_000L)
val readThread = source.listThreads(projectDir.toString(), openProject = null).single()
assertThat(readThread.activity).isEqualTo(AgentThreadActivity.READY)
assertThat(readThread.activityReport.rowActivity).isEqualTo(AgentThreadActivity.READY)
}
}
@@ -827,14 +826,14 @@ class PiSessionSourceTest {
source.setActiveThreadId("session-active-completed")
val workingThread = source.listThreads(projectDir.toString(), openProject = null).single()
assertThat(workingThread.activity).isEqualTo(AgentThreadActivity.PROCESSING)
assertThat(workingThread.activityReport.rowActivity).isEqualTo(AgentThreadActivity.PROCESSING)
appendPiSessionEntry(
sessionFile,
piAssistantMessageEntry(id = "assistant-active-completed", parentId = "user-active-completed", timestamp = 4_000L),
)
val completedThread = source.listThreads(projectDir.toString(), openProject = null).single()
assertThat(completedThread.activity).isEqualTo(AgentThreadActivity.UNREAD)
assertThat(completedThread.activityReport.rowActivity).isEqualTo(AgentThreadActivity.UNREAD)
}
}
@@ -200,6 +200,7 @@ class PiThemeSupportTest {
"from \"./jbcentral.ts\"",
"from \"./omlx.ts\"",
"from \"./status.ts\"",
"from \"./taskFolders.ts\"",
"from \"./theme.ts\"",
"controlBridge = startControlBridge(ctx)",
)
@@ -208,13 +209,14 @@ class PiThemeSupportTest {
@Test
fun bundledExtensionRegistersTaskFolderToolForPi() {
val entrypoint = readBundledPiExtensionText("agent-workbench-extension.ts")
val control = readBundledPiExtensionText("control.ts")
val taskFolders = readBundledPiExtensionText("taskFolders.ts")
assertThat(entrypoint).contains(
"import {registerTaskFolderTools, startControlBridge} from \"./control.ts\";",
"import {startControlBridge} from \"./control.ts\";",
"import {registerTaskFolderTools} from \"./taskFolders.ts\";",
"registerTaskFolderTools(pi, () => controlBridge);",
)
assertThat(control).contains(
assertThat(taskFolders).contains(
"const TASK_FOLDER_TOOL_DEFINITIONS",
"pi.registerTool(defineTool({",
"name: \"agent_workbench_get_current_task_folder\"",
@@ -227,7 +229,7 @@ class PiThemeSupportTest {
"name: Type.String({description: \"Task folder name\"})",
"metadata: Type.Optional(Type.Record(Type.String(), Type.String({description: TASK_FOLDER_METADATA_DESCRIPTION})))",
"Use metadata key 'issue' for issue tracker ids and 'review' for review ids; do not use separate issue parameters.",
"const result = await bridge.requestTaskFolder(definition.operation, definition.arguments?.(normalizedParams) ?? normalizedParams);",
"const result = await bridge.request(TASK_FOLDER_REQUEST_TYPE, {operation: definition.operation, arguments: args});",
"details: result",
)
}
@@ -4,7 +4,6 @@ package com.intellij.platform.ai.agent.terminal.sessions
// @spec community/plugins/agent-workbench/spec/sessions/agent-terminal-sessions.spec.md
// @spec community/plugins/agent-workbench/spec/core/agent-state-storage.spec.md
import com.intellij.platform.ai.agent.core.AgentThreadActivity
import com.intellij.platform.ai.agent.core.normalizeAgentWorkbenchPath
import com.intellij.platform.ai.agent.core.session.AgentSessionThread
import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalRestoreContext
@@ -191,7 +190,6 @@ private fun PersistedTerminalSessionState.toAgentSessionThread(): AgentSessionTh
title = title,
updatedAt = updatedAt,
archived = archived,
activity = AgentThreadActivity.READY,
provider = TERMINAL_AGENT_SESSION_PROVIDER,
)
}
@@ -3,7 +3,6 @@ f:com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadActivityPresent
- f:path:java.lang.String
- f:threadId:java.lang.String
- f:updatedAt:java.lang.Long
- <init>(java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.AgentThreadActivity):V
- <init>(java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.AgentThreadActivityReport,java.lang.Long):V
- b:<init>(java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.AgentThreadActivityReport,java.lang.Long,I,kotlin.jvm.internal.DefaultConstructorMarker):V
- f:component1():java.lang.String
@@ -21,7 +20,6 @@ f:com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentation
- f:activityReport:com.intellij.platform.ai.agent.core.AgentThreadActivityReport
- f:title:java.lang.String
- f:updatedAt:java.lang.Long
- <init>(java.lang.String,com.intellij.platform.ai.agent.core.AgentThreadActivity):V
- <init>(java.lang.String,com.intellij.platform.ai.agent.core.AgentThreadActivityReport,java.lang.Long):V
- b:<init>(java.lang.String,com.intellij.platform.ai.agent.core.AgentThreadActivityReport,java.lang.Long,I,kotlin.jvm.internal.DefaultConstructorMarker):V
- f:component1():java.lang.String
@@ -30,7 +28,6 @@ f:com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentation
- f:copy(java.lang.String,com.intellij.platform.ai.agent.core.AgentThreadActivityReport,java.lang.Long):com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentation
- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentation,java.lang.String,com.intellij.platform.ai.agent.core.AgentThreadActivityReport,java.lang.Long,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentation
- equals(java.lang.Object):Z
- f:getActivity():com.intellij.platform.ai.agent.core.AgentThreadActivity
- hashCode():I
f:com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationChangeSet
- sf:Companion:com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationChangeSet$Companion
@@ -80,8 +77,8 @@ f:com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationMod
- f:updateActivityHints-9c6lQVA(java.lang.String,java.util.Collection):com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationChangeSet
- f:updatePresentationHints-9c6lQVA(java.lang.String,java.util.Collection):com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationChangeSet
- f:updateProviderSnapshot-3n3rsyc(java.lang.String,java.util.Set,java.util.Map):com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationChangeSet
- f:updateThread-AlqNjB4(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.AgentThreadActivity,com.intellij.platform.ai.agent.core.AgentThreadActivityReport,java.lang.Long):com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationChangeSet
- bs:updateThread-AlqNjB4$default(com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationModel,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.AgentThreadActivity,com.intellij.platform.ai.agent.core.AgentThreadActivityReport,java.lang.Long,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationChangeSet
- f:updateThread-RmkVQ7I(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.AgentThreadActivityReport,java.lang.Long):com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationChangeSet
- bs:updateThread-RmkVQ7I$default(com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationModel,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.AgentThreadActivityReport,java.lang.Long,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationChangeSet
f:com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationPatchUpdate
- f:activityReport:com.intellij.platform.ai.agent.core.AgentThreadActivityReport
- f:path:java.lang.String
@@ -159,24 +156,6 @@ f:com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$SubAgent
- getThreadId():java.lang.String
- getTitle():java.lang.String
- hashCode():I
f:com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$TaskFolder
- com.intellij.platform.ai.agent.sessions.core.SessionActionTarget
- f:folderId:java.lang.String
- f:isDone:Z
- f:metadata:java.util.Map
- f:name:java.lang.String
- f:path:java.lang.String
- <init>(java.lang.String,java.lang.String,java.lang.String,Z,java.util.Map):V
- b:<init>(java.lang.String,java.lang.String,java.lang.String,Z,java.util.Map,I,kotlin.jvm.internal.DefaultConstructorMarker):V
- f:component1():java.lang.String
- f:component2():java.lang.String
- f:component3():java.lang.String
- f:component4():Z
- f:component5():java.util.Map
- f:copy(java.lang.String,java.lang.String,java.lang.String,Z,java.util.Map):com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$TaskFolder
- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$TaskFolder,java.lang.String,java.lang.String,java.lang.String,Z,java.util.Map,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$TaskFolder
- equals(java.lang.Object):Z
- hashCode():I
f:com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$Thread
- com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$Conversation
- b:<init>(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionThread,I,kotlin.jvm.internal.DefaultConstructorMarker):V
@@ -1,7 +1,6 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.ai.agent.sessions.core
import com.intellij.platform.ai.agent.core.AgentThreadActivity
import com.intellij.platform.ai.agent.core.AgentThreadActivityReport
import com.intellij.platform.ai.agent.core.normalizeAgentWorkbenchPath
import com.intellij.platform.ai.agent.core.session.AgentSessionProvider
@@ -42,18 +41,7 @@ data class AgentSessionThreadPresentation(
@JvmField val title: @NlsSafe String,
@JvmField val activityReport: AgentThreadActivityReport,
@JvmField val updatedAt: Long? = null,
) {
constructor(
title: @NlsSafe String,
activity: AgentThreadActivity,
) : this(
title = title,
activityReport = AgentThreadActivityReport(activity),
)
val activity: AgentThreadActivity
get() = activityReport.rowActivity
}
)
data class AgentSessionThreadPresentationChangeSet(
@JvmField val changedKeys: Set<AgentSessionThreadPresentationKey>,
@@ -75,17 +63,7 @@ data class AgentSessionThreadActivityPresentationUpdate(
@JvmField val threadId: String,
@JvmField val activityReport: AgentThreadActivityReport,
@JvmField val updatedAt: Long? = null,
) {
constructor(
path: String,
threadId: String,
activity: AgentThreadActivity,
) : this(
path = path,
threadId = threadId,
activityReport = AgentThreadActivityReport(activity),
)
}
)
@Service(Service.Level.APP)
class AgentSessionThreadPresentationModel {
@@ -105,8 +83,7 @@ class AgentSessionThreadPresentationModel {
provider: AgentSessionProvider,
threadId: String,
title: String,
activity: AgentThreadActivity?,
activityReport: AgentThreadActivityReport? = activity?.let(::AgentThreadActivityReport),
activityReport: AgentThreadActivityReport? = null,
updatedAt: Long? = null,
): AgentSessionThreadPresentationChangeSet {
val key = AgentSessionThreadPresentationKey.create(projectPath = path, provider = provider, threadId = threadId)
@@ -15,14 +15,6 @@ sealed interface SessionActionTarget {
@JvmField val path: String,
) : SessionActionTarget
data class TaskFolder(
@JvmField val path: String,
@JvmField val folderId: String,
@JvmField val name: String,
@JvmField val isDone: Boolean,
@JvmField val metadata: Map<String, String> = emptyMap(),
) : SessionActionTarget
sealed interface Conversation : SessionActionTarget {
val path: String
val provider: AgentSessionProvider
+6 -2
View File
@@ -26,8 +26,10 @@ jvm_library(
"//platform/util",
"//plugins/agent-workbench/lib-agent/core",
"//plugins/agent-workbench/lib-agent/providers/pi/sessions",
"//plugins/agent-workbench/sessions",
"//plugins/agent-workbench/lib-agent/sessions-core:ai-agent-sessions-core",
"//plugins/agent-workbench/sessions-task-folders:agent-workbench-sessions-task-folders",
"//plugins/agent-workbench/settings",
"//libraries/jackson3/jackson:libraries-jackson3",
],
)
@@ -48,8 +50,10 @@ jvm_library(
"//platform/util:util_test_lib",
"//plugins/agent-workbench/lib-agent/core:core_test_lib",
"//plugins/agent-workbench/lib-agent/providers/pi/sessions:sessions_test_lib",
"//plugins/agent-workbench/sessions:sessions_test_lib",
"//plugins/agent-workbench/lib-agent/sessions-core:ai-agent-sessions-core_test_lib",
"//plugins/agent-workbench/sessions-task-folders:agent-workbench-sessions-task-folders_test_lib",
"//plugins/agent-workbench/settings:settings_test_lib",
"//libraries/jackson3/jackson:libraries-jackson3_test_lib",
],
)
### auto-generated section `build intellij.agent.workbench.pi.awb` end
@@ -14,7 +14,9 @@
<orderEntry type="module" module-name="intellij.platform.util" />
<orderEntry type="module" module-name="intellij.platform.ai.agent.core" />
<orderEntry type="module" module-name="intellij.platform.ai.agent.pi.sessions" />
<orderEntry type="module" module-name="intellij.agent.workbench.sessions" />
<orderEntry type="module" module-name="intellij.platform.ai.agent.sessions.core" />
<orderEntry type="module" module-name="intellij.agent.workbench.sessions.task.folders" />
<orderEntry type="module" module-name="intellij.agent.workbench.settings" />
<orderEntry type="module" module-name="intellij.libraries.jackson3" />
</component>
</module>
@@ -3,12 +3,14 @@
<dependencies>
<module name="intellij.platform.ai.agent.core"/>
<module name="intellij.platform.ai.agent.pi.sessions"/>
<module name="intellij.agent.workbench.sessions"/>
<module name="intellij.platform.ai.agent.sessions.core"/>
<module name="intellij.agent.workbench.sessions.task.folders"/>
<module name="intellij.agent.workbench.settings"/>
</dependencies>
<!-- endregion -->
<extensions defaultExtensionNs="com.intellij.agent.workbench">
<settingsContributor implementation="com.intellij.platform.ai.agent.pi.sessions.PiProviderSettingsContributor"/>
<pi.controlRequestHandler implementation="com.intellij.platform.ai.agent.pi.sessions.PiTaskFolderControlHandler"/>
</extensions>
</idea-plugin>
@@ -0,0 +1,4 @@
settings.agent.workbench.provider.pi.omlx.models=Use local oMLX models
settings.agent.workbench.provider.pi.omlx.models.description=When enabled, Agent Workbench discovers local oMLX models and registers the selected model for Pi sessions.
settings.agent.workbench.provider.pi.jbcentral.models=Use JetBrains Central models
settings.agent.workbench.provider.pi.jbcentral.models.description=When enabled, Agent Workbench detects JetBrains Central wiring and registers it for Pi sessions.
@@ -2,12 +2,9 @@
package com.intellij.platform.ai.agent.pi.sessions
import com.intellij.platform.ai.agent.core.session.AgentSessionProvider
import com.intellij.agent.workbench.sessions.AgentSessionsBundle
import com.intellij.agent.workbench.settings.AgentWorkbenchCheckboxSetting
import com.intellij.agent.workbench.settings.AgentWorkbenchSettingsContributor
private val PI_AGENT_SESSION_PROVIDER: AgentSessionProvider = AgentSessionProvider.from("pi")
internal class PiProviderSettingsContributor : AgentWorkbenchSettingsContributor {
override fun providerCheckboxSettings(provider: AgentSessionProvider): List<AgentWorkbenchCheckboxSetting> {
if (provider != PI_AGENT_SESSION_PROVIDER) {
@@ -15,14 +12,14 @@ internal class PiProviderSettingsContributor : AgentWorkbenchSettingsContributor
}
return listOf(
AgentWorkbenchCheckboxSetting(
text = AgentSessionsBundle.message("settings.agent.workbench.provider.pi.omlx.models"),
description = AgentSessionsBundle.message("settings.agent.workbench.provider.pi.omlx.models.description"),
text = PiSessionsBundle.message("settings.agent.workbench.provider.pi.omlx.models"),
description = PiSessionsBundle.message("settings.agent.workbench.provider.pi.omlx.models.description"),
isSelected = PiOmlxSupportSettings::isEnabled,
setSelected = PiOmlxSupportSettings::setEnabled,
),
AgentWorkbenchCheckboxSetting(
text = AgentSessionsBundle.message("settings.agent.workbench.provider.pi.jbcentral.models"),
description = AgentSessionsBundle.message("settings.agent.workbench.provider.pi.jbcentral.models.description"),
text = PiSessionsBundle.message("settings.agent.workbench.provider.pi.jbcentral.models"),
description = PiSessionsBundle.message("settings.agent.workbench.provider.pi.jbcentral.models.description"),
isSelected = PiJbCentralSupportSettings::isEnabled,
setSelected = PiJbCentralSupportSettings::setEnabled,
)
@@ -0,0 +1,18 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.ai.agent.pi.sessions
import com.intellij.DynamicBundle
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.NonNls
import org.jetbrains.annotations.PropertyKey
const val PI_SESSIONS_BUNDLE: @NonNls String = "messages.PiSessionsBundle"
internal object PiSessionsBundle {
private val BUNDLE = DynamicBundle(PiSessionsBundle::class.java, PI_SESSIONS_BUNDLE)
fun message(key: @PropertyKey(resourceBundle = PI_SESSIONS_BUNDLE) String, vararg params: Any): @Nls String {
return BUNDLE.getMessage(key, *params)
}
}
@@ -1,29 +1,43 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.ai.agent.pi.sessions
import com.intellij.agent.workbench.sessions.model.ArchiveThreadTarget
import com.intellij.agent.workbench.sessions.service.AgentSessionArchiveService
import com.intellij.agent.workbench.sessions.statistics.AgentWorkbenchEntryPoint
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolder
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolderService
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolderStatus
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolderThreadAssignment
import com.intellij.openapi.components.service
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolder
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderService
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderStatus
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderThreadAssignment
import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.platform.ai.agent.core.session.AgentSessionProvider
import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProviders
import kotlinx.coroutines.CancellationException
import org.jetbrains.annotations.ApiStatus
import tools.jackson.core.JsonGenerator
internal class PiTaskFolderControlHandler(
private val taskFolderServiceProvider: () -> AgentTaskFolderService = { service<AgentTaskFolderService>() },
private val archiveServiceProvider: () -> AgentSessionArchiveService = { service<AgentSessionArchiveService>() },
) {
fun handle(
@ApiStatus.Internal
class PiTaskFolderControlHandler : PiControlRequestHandler {
private val taskFolderServiceProvider: () -> AgentTaskFolderService
constructor() : this(
taskFolderServiceProvider = { service<AgentTaskFolderService>() },
)
internal constructor(
taskFolderServiceProvider: () -> AgentTaskFolderService,
) {
this.taskFolderServiceProvider = taskFolderServiceProvider
}
override val messageType: String = PI_TASK_FOLDER_CONTROL_MESSAGE_TYPE
override fun handle(
context: PiControlSessionContext,
payload: PiControlPayload,
request: PiControlExtensionRequest,
requestId: String,
sendResponse: (String) -> Unit,
) {
val service = taskFolderServiceProvider()
val arguments = payload.arguments ?: PiTaskFolderControlArguments()
when (payload.operation?.trim()) {
val arguments = request.arguments ?: PiControlRequestArguments()
when (request.operation?.trim()) {
OP_GET_CURRENT -> {
val folder = service.getFolderForThread(context.projectPath, PI_AGENT_SESSION_PROVIDER, context.sessionId)
sendResponse(buildTaskFolderResponse(requestId = requestId, folder = folder))
@@ -107,7 +121,7 @@ internal class PiTaskFolderControlHandler(
sendResponse(buildPiControlErrorResponse(requestId, "Task folder is not available"))
}
else {
markDone(service, archiveServiceProvider, context, folder, requestId, sendResponse)
markDone(service, context, folder, requestId, sendResponse)
}
}
OP_DELETE -> {
@@ -128,7 +142,7 @@ internal class PiTaskFolderControlHandler(
private fun createAndAssignCurrentThread(
service: AgentTaskFolderService,
context: PiControlSessionContext,
arguments: PiTaskFolderControlArguments,
arguments: PiControlRequestArguments,
requestId: String,
sendResponse: (String) -> Unit,
) {
@@ -154,7 +168,6 @@ private fun createAndAssignCurrentThread(
private fun markDone(
service: AgentTaskFolderService,
archiveServiceProvider: () -> AgentSessionArchiveService,
context: PiControlSessionContext,
folder: AgentTaskFolder,
requestId: String,
@@ -169,8 +182,8 @@ private fun markDone(
return
}
val targets = service.listFolderThreadAssignments(folder.id)
.map { assignment -> ArchiveThreadTarget.Thread(assignment.path, assignment.provider, assignment.threadId) }
.distinctBy { target -> ArchiveTargetKey(target.path, target.provider.value, target.threadId) }
.map { assignment -> TaskFolderArchiveTarget(assignment.path, assignment.provider, assignment.threadId) }
.distinct()
if (targets.isEmpty()) {
val changed = service.setFolderStatus(folder.id, AgentTaskFolderStatus.DONE)
sendResponse(
@@ -184,23 +197,38 @@ private fun markDone(
)
return
}
val archiveService = archiveServiceProvider()
if (!targets.all { target -> archiveService.canArchiveProvider(target.provider) }) {
if (!targets.all { target -> AgentSessionProviders.find(target.provider)?.supportsArchiveThread == true }) {
sendResponse(buildPiControlErrorResponse(requestId, "Task folder contains threads that cannot be archived"))
return
}
archiveService.archiveThreads(targets, AgentWorkbenchEntryPoint.TREE_POPUP) { result ->
val changed = result.allRequestedArchived && service.setFolderStatus(folder.id, AgentTaskFolderStatus.DONE)
sendResponse(
buildTaskFolderDoneResponse(
requestId = requestId,
changed = changed,
folder = refreshedFolder(service, context, folder.id) ?: folder,
requestedCount = result.requestedCount,
archivedCount = result.archivedCount,
)
val result = archiveTargets(targets)
val changed = result.allRequestedArchived && service.setFolderStatus(folder.id, AgentTaskFolderStatus.DONE)
sendResponse(
buildTaskFolderDoneResponse(
requestId = requestId,
changed = changed,
folder = refreshedFolder(service, context, folder.id) ?: folder,
requestedCount = result.requestedCount,
archivedCount = result.archivedCount,
)
)
}
private fun archiveTargets(targets: List<TaskFolderArchiveTarget>): TaskFolderArchiveResult {
var archivedCount = 0
targets.forEach { target ->
val descriptor = AgentSessionProviders.find(target.provider)
if (descriptor?.supportsArchiveThread != true) return@forEach
val archived = try {
runBlockingCancellable { descriptor.archiveThread(path = target.path, threadId = target.threadId) }
}
catch (t: Throwable) {
if (t is CancellationException) throw t
false
}
if (archived) archivedCount++
}
return TaskFolderArchiveResult(requestedCount = targets.size, archivedCount = archivedCount)
}
private fun buildTaskFolderCreatedResponse(requestId: String, folder: AgentTaskFolder, created: Boolean, assigned: Boolean): String {
@@ -262,15 +290,7 @@ private fun buildTaskFolderDoneResponse(
}
private fun buildTaskFolderResultResponse(requestId: String, writeResult: (JsonGenerator) -> Unit): String {
return buildPiControlJsonObject { generator ->
generator.writeStringProperty("type", PiControlMessageType.RESPONSE.wireName)
generator.writeStringProperty("requestId", requestId)
generator.writeBooleanProperty("ok", true)
generator.writeName("result")
generator.writeStartObject()
writeResult(generator)
generator.writeEndObject()
}
return buildPiControlResultResponse(requestId, writeResult)
}
private fun writeTaskFolder(generator: JsonGenerator, folder: AgentTaskFolder?) {
@@ -305,7 +325,7 @@ private fun writeTaskFolderAssignment(generator: JsonGenerator, assignment: Agen
private fun resolveTaskFolder(
service: AgentTaskFolderService,
context: PiControlSessionContext,
arguments: PiTaskFolderControlArguments,
arguments: PiControlRequestArguments,
): AgentTaskFolder? {
val explicitFolderId = arguments.folderId?.trim()?.takeIf { it.isNotEmpty() }
if (explicitFolderId != null) {
@@ -318,12 +338,20 @@ private fun refreshedFolder(service: AgentTaskFolderService, context: PiControlS
return service.snapshot(includeDone = true).folder(context.projectPath, folderId)
}
private data class ArchiveTargetKey(
private data class TaskFolderArchiveTarget(
@JvmField val path: String,
@JvmField val providerId: String,
val provider: AgentSessionProvider,
@JvmField val threadId: String,
)
private data class TaskFolderArchiveResult(
@JvmField val requestedCount: Int,
@JvmField val archivedCount: Int,
) {
val allRequestedArchived: Boolean
get() = requestedCount > 0 && requestedCount == archivedCount
}
private const val OP_GET_CURRENT: String = "getCurrent"
private const val OP_LIST_FOLDERS: String = "listFolders"
private const val OP_LIST_THREADS: String = "listThreads"
@@ -335,3 +363,4 @@ private const val OP_SET_METADATA: String = "setMetadata"
private const val OP_DELETE_METADATA: String = "deleteMetadata"
private const val OP_MARK_DONE: String = "markDone"
private const val OP_DELETE: String = "delete"
private const val PI_TASK_FOLDER_CONTROL_MESSAGE_TYPE: String = "taskFolderRequest"
@@ -27,6 +27,7 @@
- name: intellij.agent.workbench.prompt.vcs.ui
- name: intellij.agent.workbench.prompt.testrunner
- name: intellij.agent.workbench.sessions
- name: intellij.agent.workbench.sessions.task.folders
- name: intellij.agent.workbench.sessions.jbcentral
- name: intellij.agent.workbench.sessions.actions
- name: intellij.agent.workbench.sessions.toolwindow
@@ -272,6 +272,8 @@ jvm_library(
"//plugins/agent-workbench/lib-agent/providers/codex/common:ai-agent-codex-common-tests_test_lib",
"//plugins/agent-workbench/sessions-launch-config/backend:agent-workbench-sessions-launch-config-backend-tests",
"//plugins/agent-workbench/sessions-launch-config/backend:agent-workbench-sessions-launch-config-backend-tests_test_lib",
"//plugins/agent-workbench/sessions-task-folders:agent-workbench-sessions-task-folders-tests",
"//plugins/agent-workbench/sessions-task-folders:agent-workbench-sessions-task-folders-tests_test_lib",
],
)
### auto-generated section `build intellij.agent.workbench.plugin.tests` end
@@ -93,6 +93,7 @@
<orderEntry type="module" module-name="intellij.agent.workbench.sessions.cost.tests" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.ai.agent.codex.common.tests" scope="TEST" />
<orderEntry type="module" module-name="intellij.agent.workbench.sessions.launch.config.backend.tests" scope="TEST" />
<orderEntry type="module" module-name="intellij.agent.workbench.sessions.task.folders.tests" scope="TEST" />
</component>
<component name="TestModuleProperties" production-module="intellij.agent.workbench.plugin" />
</module>
@@ -80,6 +80,9 @@
- name: lib/modules/intellij.agent.workbench.sessions.launch.config.backend.jar
contentModules:
- name: intellij.agent.workbench.sessions.launch.config.backend
- name: lib/modules/intellij.agent.workbench.sessions.task.folders.jar
contentModules:
- name: intellij.agent.workbench.sessions.task.folders
- name: lib/modules/intellij.agent.workbench.sessions.toolwindow.jar
contentModules:
- name: intellij.agent.workbench.sessions.toolwindow
@@ -49,6 +49,7 @@
<module name="intellij.agent.workbench.prompt.vcs.ui"/>
<module name="intellij.agent.workbench.prompt.testrunner"/>
<module name="intellij.agent.workbench.sessions"/>
<module name="intellij.agent.workbench.sessions.task.folders"/>
<module name="intellij.agent.workbench.sessions.jbcentral"/>
<module name="intellij.agent.workbench.ui"/>
<module name="intellij.agent.workbench.sessions.actions"/>
@@ -218,7 +218,7 @@ internal fun formatExistingTaskEntries(
nowLabel = nowLabel,
unknownLabel = unknownLabel,
),
activity = thread.activity,
activity = thread.activityReport.rowActivity,
)
}
.toList()
@@ -2,6 +2,7 @@
package com.intellij.agent.workbench.prompt.ui
import com.intellij.platform.ai.agent.core.AgentThreadActivity
import com.intellij.platform.ai.agent.core.AgentThreadActivityReport
import com.intellij.platform.ai.agent.core.session.AgentSessionProvider
import com.intellij.platform.ai.agent.core.session.AgentSessionThread
import com.intellij.agent.workbench.prompt.core.AgentPromptExistingThreadsSnapshot
@@ -311,7 +312,7 @@ class AgentPromptExistingTaskControllerTest {
title = title,
updatedAt = updatedAt,
archived = false,
activity = activity,
activityReport = AgentThreadActivityReport(activity),
provider = AgentSessionProvider.from("codex"),
)
}
@@ -2,6 +2,7 @@
package com.intellij.agent.workbench.prompt.ui
import com.intellij.platform.ai.agent.core.AgentThreadActivity
import com.intellij.platform.ai.agent.core.AgentThreadActivityReport
import com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode
import com.intellij.platform.ai.agent.core.session.AgentSessionProvider
import com.intellij.platform.ai.agent.core.session.AgentSessionThread
@@ -543,7 +544,7 @@ class AgentPromptPaletteSessionControllerTest {
title = "${provider.value} thread",
updatedAt = 100,
archived = false,
activity = AgentThreadActivity.READY,
activityReport = AgentThreadActivityReport(AgentThreadActivity.READY),
provider = provider,
)
}
@@ -25,6 +25,7 @@ import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.util.Disposer
import com.intellij.platform.ai.agent.common.session.isClaudeMenuCommandPrompt
import com.intellij.platform.ai.agent.core.AgentThreadActivity
import com.intellij.platform.ai.agent.core.AgentThreadActivityReport
import com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode
import com.intellij.platform.ai.agent.core.session.AgentSessionProvider
import com.intellij.platform.ai.agent.core.session.AgentSessionThread
@@ -682,7 +683,7 @@ class AgentPromptPaletteSubmitControllerTest {
updatedAt = 100,
archived = false,
provider = AgentSessionProvider.from("codex"),
activity = AgentThreadActivity.PROCESSING,
activityReport = AgentThreadActivityReport(AgentThreadActivity.PROCESSING),
subAgents = emptyList(),
)
),
@@ -0,0 +1,136 @@
load("@community//build:tests-options.bzl", "jps_test")
load("@rules_jvm//:jvm.bzl", "jvm_library")
### auto-generated section `build intellij.agent.workbench.sessions.task.folders` start
jvm_library(
name = "agent-workbench-sessions-task-folders",
srcs = glob(
[
"src/**/*.kt",
"src/**/*.java",
"src/**/*.form",
],
allow_empty = True,
),
module_name = "intellij.agent.workbench.sessions.task.folders",
resource_strip_prefix = "resources",
resources = glob(
["resources/**/*"],
),
visibility = ["//visibility:public"],
deps = [
# do not sort,
"@lib//:kotlin-stdlib",
"//libraries/kotlinx/serialization/core",
"//platform/core-api:core",
"//platform/projectModel-api:projectModel",
"//platform/util",
"//platform/util-ex",
"//plugins/agent-workbench/lib-agent/core",
],
)
jvm_library(
name = "agent-workbench-sessions-task-folders_test_lib",
testonly = True,
srcs = glob(
[],
allow_empty = True,
),
module_name = "intellij.agent.workbench.sessions.task.folders",
visibility = ["//visibility:public"],
runtime_deps = [
# do not sort,
":agent-workbench-sessions-task-folders",
"//libraries/kotlinx/serialization/core:core_test_lib",
"//platform/core-api:core_test_lib",
"//platform/projectModel-api:projectModel_test_lib",
"//platform/util:util_test_lib",
"//platform/util-ex:util-ex_test_lib",
"//plugins/agent-workbench/lib-agent/core:core_test_lib",
],
)
### auto-generated section `build intellij.agent.workbench.sessions.task.folders` end
### auto-generated section `iml intellij.agent.workbench.sessions.task.folders` start
exports_files(
[
"intellij.agent.workbench.sessions.task.folders.iml",
],
visibility = ["//visibility:public"],
)
### auto-generated section `iml intellij.agent.workbench.sessions.task.folders` end
### auto-generated section `build intellij.agent.workbench.sessions.task.folders.tests` start
jvm_library(
name = "agent-workbench-sessions-task-folders-tests",
srcs = glob(
[],
allow_empty = True,
),
module_name = "intellij.agent.workbench.sessions.task.folders.tests",
visibility = ["//visibility:public"],
)
jvm_library(
name = "agent-workbench-sessions-task-folders-tests_test_lib",
testonly = True,
srcs = glob(
[
"testSrc/**/*.kt",
"testSrc/**/*.java",
"testSrc/**/*.form",
],
allow_empty = True,
),
associates = [
"//plugins/agent-workbench/sessions-task-folders:agent-workbench-sessions-task-folders",
"//plugins/agent-workbench/sessions-task-folders:agent-workbench-sessions-task-folders_test_lib",
],
visibility = ["//visibility:public"],
runtime_deps = [":agent-workbench-sessions-task-folders-tests"],
deps = [
# do not sort,
"@lib//:kotlin-stdlib",
"//libraries/kotlinx/serialization/core",
"//libraries/kotlinx/serialization/core:core_test_lib",
"//platform/core-api:core",
"//platform/core-api:core_test_lib",
"//platform/projectModel-api:projectModel",
"//platform/projectModel-api:projectModel_test_lib",
"//platform/util",
"//platform/util:util_test_lib",
"//platform/util-ex",
"//platform/util-ex:util-ex_test_lib",
"//plugins/agent-workbench/lib-agent/core",
"//plugins/agent-workbench/lib-agent/core:core_test_lib",
"//libraries/junit5",
"//libraries/junit5:junit5_test_lib",
"//platform/testFramework",
"//platform/testFramework:testFramework_test_lib",
"//platform/testFramework/junit5",
"//platform/testFramework/junit5:junit5_test_lib",
"//libraries/assertj-core",
"//libraries/assertj-core:assertj-core_test_lib",
],
)
### auto-generated section `build intellij.agent.workbench.sessions.task.folders.tests` end
### auto-generated section `iml intellij.agent.workbench.sessions.task.folders.tests` start
exports_files(
[
"intellij.agent.workbench.sessions.task.folders.tests.iml",
],
visibility = ["//visibility:public"],
)
### auto-generated section `iml intellij.agent.workbench.sessions.task.folders.tests` end
### auto-generated section `test intellij.agent.workbench.sessions.task.folders.tests` start
jps_test(
name = "agent-workbench-sessions-task-folders-tests_test",
runtime_deps = [":agent-workbench-sessions-task-folders-tests_test_lib"],
)
### auto-generated section `test intellij.agent.workbench.sessions.task.folders.tests` end
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="kotlin-language" name="Kotlin">
<configuration version="5" platform="JVM 25" allPlatforms="JVM [25]" useProjectSettings="false">
<compilerSettings>
<option name="additionalArguments" value="-Xjvm-default=all -XXLanguage:+AllowEagerSupertypeAccessibilityChecks -progressive" />
</compilerSettings>
<compilerArguments>
<stringArguments>
<stringArg name="jvmTarget" arg="25" />
<stringArg name="apiVersion" arg="2.4" />
<stringArg name="languageVersion" arg="2.4" />
</stringArguments>
<arrayArguments>
<arrayArg name="pluginClasspaths">
<args>$KOTLIN_BUNDLED$/lib/kotlinx-serialization-compiler-plugin.jar</args>
</arrayArg>
</arrayArguments>
</compilerArguments>
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="com.intellij.agent.workbench.sessions.task.folders" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="module" module-name="intellij.libraries.kotlinx.serialization.core" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.platform.projectModel" />
<orderEntry type="module" module-name="intellij.platform.util" />
<orderEntry type="module" module-name="intellij.platform.util.ex" />
<orderEntry type="module" module-name="intellij.platform.ai.agent.core" />
</component>
</module>
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="kotlin-language" name="Kotlin">
<configuration version="5" platform="JVM 25" allPlatforms="JVM [25]" useProjectSettings="false">
<compilerSettings>
<option name="additionalArguments" value="-Xjvm-default=all -XXLanguage:+AllowEagerSupertypeAccessibilityChecks -progressive" />
</compilerSettings>
<compilerArguments>
<stringArguments>
<stringArg name="jvmTarget" arg="25" />
<stringArg name="apiVersion" arg="2.4" />
<stringArg name="languageVersion" arg="2.4" />
</stringArguments>
<arrayArguments>
<arrayArg name="pluginClasspaths">
<args>$KOTLIN_BUNDLED$/lib/kotlinx-serialization-compiler-plugin.jar</args>
</arrayArg>
</arrayArguments>
</compilerArguments>
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$/testSrc">
<sourceFolder url="file://$MODULE_DIR$/testSrc" isTestSource="true" packagePrefix="com.intellij.agent.workbench.sessions.task.folders" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" scope="TEST" name="kotlin-stdlib" level="project" />
<orderEntry type="module" module-name="intellij.libraries.kotlinx.serialization.core" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.core" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.projectModel" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.util" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.util.ex" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.ai.agent.core" scope="TEST" />
<orderEntry type="module" module-name="intellij.libraries.junit5" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.testFramework.junit5" scope="TEST" />
<orderEntry type="module" module-name="intellij.libraries.assertj.core" scope="TEST" />
<orderEntry type="module" module-name="intellij.agent.workbench.sessions.task.folders" scope="TEST" />
</component>
<component name="TestModuleProperties" production-module="intellij.agent.workbench.sessions.task.folders" />
</module>
@@ -0,0 +1,7 @@
<idea-plugin>
<!-- region Generated dependencies - run `Generate Product Layouts` to regenerate -->
<dependencies>
<module name="intellij.platform.ai.agent.core"/>
</dependencies>
<!-- endregion -->
</idea-plugin>
@@ -1,5 +1,5 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.ai.agent.sessions.core.folders
package com.intellij.agent.workbench.sessions.task.folders
import com.intellij.platform.ai.agent.core.normalizeAgentWorkbenchPath
import com.intellij.platform.ai.agent.core.session.AgentSessionProvider
@@ -1,9 +1,7 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.agent.workbench.sessions
package com.intellij.agent.workbench.sessions.task.folders
import com.intellij.platform.ai.agent.core.session.AgentSessionProvider
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderService
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderStatus
import com.intellij.testFramework.junit5.TestApplication
import com.intellij.util.io.Ksuid
import org.assertj.core.api.Assertions.assertThat
@@ -48,6 +48,7 @@ jvm_library(
"//platform/editor-ui-api:editor-ui",
"//plugins/agent-workbench/chat",
"//plugins/agent-workbench/sessions",
"//plugins/agent-workbench/sessions-task-folders:agent-workbench-sessions-task-folders",
"//plugins/agent-workbench/sessions-jbcentral:agent-workbench-sessions-jbcentral",
"//plugins/agent-workbench/sessions-actions:agent-workbench-sessions-actions",
"//libraries/jackson3/jackson:libraries-jackson3",
@@ -101,6 +102,7 @@ jvm_library(
"//plugins/agent-workbench/lib-agent/providers/claude/sessions:sessions_test_lib",
"//plugins/agent-workbench/chat:chat_test_lib",
"//plugins/agent-workbench/sessions:sessions_test_lib",
"//plugins/agent-workbench/sessions-task-folders:agent-workbench-sessions-task-folders_test_lib",
"//plugins/agent-workbench/sessions-jbcentral:agent-workbench-sessions-jbcentral_test_lib",
"//plugins/agent-workbench/sessions-actions:agent-workbench-sessions-actions_test_lib",
"//libraries/jackson3/jackson:libraries-jackson3_test_lib",
@@ -222,6 +224,8 @@ jvm_library(
"//plugins/agent-workbench/chat:chat_test_lib",
"//plugins/agent-workbench/sessions",
"//plugins/agent-workbench/sessions:sessions_test_lib",
"//plugins/agent-workbench/sessions-task-folders:agent-workbench-sessions-task-folders",
"//plugins/agent-workbench/sessions-task-folders:agent-workbench-sessions-task-folders_test_lib",
"//plugins/agent-workbench/sessions-jbcentral:agent-workbench-sessions-jbcentral",
"//plugins/agent-workbench/sessions-jbcentral:agent-workbench-sessions-jbcentral_test_lib",
"//plugins/agent-workbench/sessions-actions:agent-workbench-sessions-actions",
@@ -59,6 +59,7 @@
<orderEntry type="module" module-name="intellij.platform.ai.agent.claude.sessions" scope="TEST" />
<orderEntry type="module" module-name="intellij.agent.workbench.chat" />
<orderEntry type="module" module-name="intellij.agent.workbench.sessions" />
<orderEntry type="module" module-name="intellij.agent.workbench.sessions.task.folders" />
<orderEntry type="module" module-name="intellij.agent.workbench.sessions.jbcentral" />
<orderEntry type="module" module-name="intellij.agent.workbench.sessions.actions" />
<orderEntry type="module" module-name="intellij.libraries.jackson3" />
@@ -57,6 +57,7 @@
<orderEntry type="module" module-name="intellij.platform.ai.agent.claude.sessions" scope="TEST" />
<orderEntry type="module" module-name="intellij.agent.workbench.chat" scope="TEST" />
<orderEntry type="module" module-name="intellij.agent.workbench.sessions" scope="TEST" />
<orderEntry type="module" module-name="intellij.agent.workbench.sessions.task.folders" scope="TEST" />
<orderEntry type="module" module-name="intellij.agent.workbench.sessions.jbcentral" scope="TEST" />
<orderEntry type="module" module-name="intellij.agent.workbench.sessions.actions" scope="TEST" />
<orderEntry type="module" module-name="intellij.libraries.jackson3" scope="TEST" />
@@ -8,6 +8,7 @@
<module name="intellij.platform.ai.agent.core"/>
<module name="intellij.agent.workbench.prompt.core"/>
<module name="intellij.agent.workbench.sessions"/>
<module name="intellij.agent.workbench.sessions.task.folders"/>
<module name="intellij.agent.workbench.sessions.actions"/>
<module name="intellij.platform.ai.agent.sessions.core"/>
<module name="intellij.agent.workbench.sessions.jbcentral"/>
@@ -44,7 +44,8 @@ internal object AgentSessionsTreePopupDataKeys {
internal data class AgentSessionsTreePopupActionContext(
@JvmField val project: Project,
val target: SessionActionTarget,
val target: SessionActionTarget? = null,
@JvmField val taskFolderTarget: AgentTaskFolderActionTarget? = null,
@JvmField val archiveTargets: List<ArchiveThreadTarget>,
@JvmField val unarchiveTargets: List<ArchiveThreadTarget> = emptyList(),
@JvmField val selectedThreadTargets: List<SessionActionTarget.Thread> = emptyList(),
@@ -255,7 +256,15 @@ internal class AgentSessionsTreePopupNewThreadGroup @JvmOverloads constructor(
)
}
private fun newThreadPathFromTarget(target: SessionActionTarget): String? {
internal data class AgentTaskFolderActionTarget(
@JvmField val path: String,
@JvmField val folderId: String,
@JvmField val name: String,
@JvmField val isDone: Boolean,
@JvmField val metadata: Map<String, String> = emptyMap(),
)
private fun newThreadPathFromTarget(target: SessionActionTarget?): String? {
return when (target) {
is SessionActionTarget.Project -> target.path
is SessionActionTarget.Worktree -> target.path
@@ -263,7 +272,7 @@ private fun newThreadPathFromTarget(target: SessionActionTarget): String? {
}
}
private fun morePopupLabel(target: SessionActionTarget): @Nls String {
private fun morePopupLabel(target: SessionActionTarget?): @Nls String {
return when (target) {
is SessionActionTarget.MoreProjects -> AgentSessionsBundle.message("toolwindow.action.more.count", target.hiddenCount)
is SessionActionTarget.MoreThreads ->
@@ -7,6 +7,9 @@ import com.intellij.agent.workbench.sessions.service.AgentSessionArchiveRequestR
import com.intellij.agent.workbench.sessions.service.AgentSessionArchiveService
import com.intellij.agent.workbench.sessions.statistics.AgentWorkbenchEntryPoint
import com.intellij.agent.workbench.prompt.core.AgentPromptLaunchProfile
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolder
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolderService
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolderStatus
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
@@ -24,9 +27,6 @@ import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.util.NlsSafe
import com.intellij.platform.ai.agent.core.session.AgentSessionProvider
import com.intellij.platform.ai.agent.sessions.core.SessionActionTarget
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolder
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderService
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderStatus
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBTextField
import com.intellij.util.ui.JBUI
@@ -93,17 +93,17 @@ internal class AgentSessionsTreePopupRenameTaskFolderAction : DumbAwareAction()
private val resolveContext: (AnActionEvent) -> AgentSessionsTreePopupActionContext? =
::resolveAgentSessionsTreePopupActionContext
private val promptForName: (Project, String) -> String? = ::showRenameTaskFolderDialog
private val renameFolder: (SessionActionTarget.TaskFolder, String) -> Unit = { target, name ->
private val renameFolder: (AgentTaskFolderActionTarget, String) -> Unit = { target, name ->
service<AgentTaskFolderService>().renameFolder(target.folderId, name)
}
override fun update(e: AnActionEvent) {
e.presentation.isEnabledAndVisible = resolveContext(e)?.target is SessionActionTarget.TaskFolder
e.presentation.isEnabledAndVisible = resolveContext(e)?.taskFolderTarget != null
}
override fun actionPerformed(e: AnActionEvent) {
val context = resolveContext(e) ?: return
val target = context.target as? SessionActionTarget.TaskFolder ?: return
val target = context.taskFolderTarget ?: return
val name = promptForName(context.project, target.name) ?: return
renameFolder(target, name)
}
@@ -114,18 +114,18 @@ internal class AgentSessionsTreePopupRenameTaskFolderAction : DumbAwareAction()
internal class AgentSessionsTreePopupDeleteTaskFolderAction : DumbAwareAction() {
private val resolveContext: (AnActionEvent) -> AgentSessionsTreePopupActionContext? =
::resolveAgentSessionsTreePopupActionContext
private val confirmDelete: (Project, SessionActionTarget.TaskFolder) -> Boolean = ::confirmDeleteTaskFolder
private val deleteFolder: (SessionActionTarget.TaskFolder) -> Unit = { target ->
private val confirmDelete: (Project, AgentTaskFolderActionTarget) -> Boolean = ::confirmDeleteTaskFolder
private val deleteFolder: (AgentTaskFolderActionTarget) -> Unit = { target ->
service<AgentTaskFolderService>().deleteFolder(target.folderId)
}
override fun update(e: AnActionEvent) {
e.presentation.isEnabledAndVisible = resolveContext(e)?.target is SessionActionTarget.TaskFolder
e.presentation.isEnabledAndVisible = resolveContext(e)?.taskFolderTarget != null
}
override fun actionPerformed(e: AnActionEvent) {
val context = resolveContext(e) ?: return
val target = context.target as? SessionActionTarget.TaskFolder ?: return
val target = context.taskFolderTarget ?: return
if (confirmDelete(context.project, target)) {
deleteFolder(target)
}
@@ -205,8 +205,8 @@ internal class AgentSessionsTreePopupRemoveFromTaskFolderAction : DumbAwareActio
internal class AgentSessionsTreePopupSetTaskFolderMetadataAction : DumbAwareAction {
private val resolveContext: (AnActionEvent) -> AgentSessionsTreePopupActionContext?
private val promptForMetadata: (Project, SessionActionTarget.TaskFolder) -> TaskFolderMetadataEdit?
private val setMetadata: (SessionActionTarget.TaskFolder, String, String) -> Unit
private val promptForMetadata: (Project, AgentTaskFolderActionTarget) -> TaskFolderMetadataEdit?
private val setMetadata: (AgentTaskFolderActionTarget, String, String) -> Unit
@Suppress("unused")
constructor() {
@@ -217,8 +217,8 @@ internal class AgentSessionsTreePopupSetTaskFolderMetadataAction : DumbAwareActi
internal constructor(
resolveContext: (AnActionEvent) -> AgentSessionsTreePopupActionContext?,
promptForMetadata: (Project, SessionActionTarget.TaskFolder) -> TaskFolderMetadataEdit?,
setMetadata: (SessionActionTarget.TaskFolder, String, String) -> Unit,
promptForMetadata: (Project, AgentTaskFolderActionTarget) -> TaskFolderMetadataEdit?,
setMetadata: (AgentTaskFolderActionTarget, String, String) -> Unit,
) {
this.resolveContext = resolveContext
this.promptForMetadata = promptForMetadata
@@ -226,12 +226,12 @@ internal class AgentSessionsTreePopupSetTaskFolderMetadataAction : DumbAwareActi
}
override fun update(e: AnActionEvent) {
e.presentation.isEnabledAndVisible = resolveContext(e)?.target is SessionActionTarget.TaskFolder
e.presentation.isEnabledAndVisible = resolveContext(e)?.taskFolderTarget != null
}
override fun actionPerformed(e: AnActionEvent) {
val context = resolveContext(e) ?: return
val target = context.target as? SessionActionTarget.TaskFolder ?: return
val target = context.taskFolderTarget ?: return
val metadata = promptForMetadata(context.project, target)?.let(::resolveTaskFolderMetadataUpdate) ?: return
setMetadata(target, metadata.key, metadata.value)
}
@@ -264,19 +264,19 @@ internal fun resolveTaskFolderMetadataUpdate(edit: TaskFolderMetadataEdit): Task
internal class AgentSessionsTreePopupDeleteTaskFolderMetadataAction : DumbAwareAction() {
private val resolveContext: (AnActionEvent) -> AgentSessionsTreePopupActionContext? =
::resolveAgentSessionsTreePopupActionContext
private val promptForKey: (Project, SessionActionTarget.TaskFolder) -> String? = ::showDeleteTaskFolderMetadataDialog
private val deleteMetadata: (SessionActionTarget.TaskFolder, String) -> Unit = { target, key ->
private val promptForKey: (Project, AgentTaskFolderActionTarget) -> String? = ::showDeleteTaskFolderMetadataDialog
private val deleteMetadata: (AgentTaskFolderActionTarget, String) -> Unit = { target, key ->
service<AgentTaskFolderService>().deleteMetadata(target.folderId, key)
}
override fun update(e: AnActionEvent) {
val target = resolveContext(e)?.target as? SessionActionTarget.TaskFolder
val target = resolveContext(e)?.taskFolderTarget
e.presentation.isEnabledAndVisible = target?.metadata?.isNotEmpty() == true
}
override fun actionPerformed(e: AnActionEvent) {
val context = resolveContext(e) ?: return
val target = context.target as? SessionActionTarget.TaskFolder ?: return
val target = context.taskFolderTarget ?: return
val key = promptForKey(context.project, target) ?: return
deleteMetadata(target, key)
}
@@ -288,7 +288,7 @@ internal class AgentSessionsTreePopupMarkTaskFolderDoneAction : DumbAwareAction
private val resolveContext: (AnActionEvent) -> AgentSessionsTreePopupActionContext?
private val canArchiveProvider: (AgentSessionProvider) -> Boolean
private val archiveThreads: (List<ArchiveThreadTarget>, AgentWorkbenchEntryPoint, (AgentSessionArchiveRequestResult) -> Unit) -> Unit
private val setFolderDone: (SessionActionTarget.TaskFolder) -> Unit
private val setFolderDone: (AgentTaskFolderActionTarget) -> Unit
@Suppress("unused")
constructor() {
@@ -305,7 +305,7 @@ internal class AgentSessionsTreePopupMarkTaskFolderDoneAction : DumbAwareAction
resolveContext: (AnActionEvent) -> AgentSessionsTreePopupActionContext?,
canArchiveProvider: (AgentSessionProvider) -> Boolean,
archiveThreads: (List<ArchiveThreadTarget>, AgentWorkbenchEntryPoint, (AgentSessionArchiveRequestResult) -> Unit) -> Unit,
setFolderDone: (SessionActionTarget.TaskFolder) -> Unit,
setFolderDone: (AgentTaskFolderActionTarget) -> Unit,
) {
this.resolveContext = resolveContext
this.canArchiveProvider = canArchiveProvider
@@ -315,7 +315,7 @@ internal class AgentSessionsTreePopupMarkTaskFolderDoneAction : DumbAwareAction
override fun update(e: AnActionEvent) {
val context = resolveContext(e)
val target = context?.target as? SessionActionTarget.TaskFolder
val target = context?.taskFolderTarget
if (target == null || target.isDone) {
e.presentation.isEnabledAndVisible = false
return
@@ -327,7 +327,7 @@ internal class AgentSessionsTreePopupMarkTaskFolderDoneAction : DumbAwareAction
override fun actionPerformed(e: AnActionEvent) {
val context = resolveContext(e) ?: return
val target = context.target as? SessionActionTarget.TaskFolder ?: return
val target = context.taskFolderTarget ?: return
if (target.isDone) return
val archiveTargets = context.taskFolderArchiveTargets
if (archiveTargets.isEmpty()) {
@@ -426,7 +426,7 @@ private fun showTaskFolderNameDialog(
return Messages.showInputDialog(project, message, title, Messages.getQuestionIcon(), initialValue, NonBlankInputValidator())
}
private fun confirmDeleteTaskFolder(project: Project, target: SessionActionTarget.TaskFolder): Boolean {
private fun confirmDeleteTaskFolder(project: Project, target: AgentTaskFolderActionTarget): Boolean {
return Messages.showYesNoDialog(
project,
AgentSessionsBundle.message("toolwindow.task.folder.delete.dialog.message", target.name),
@@ -435,12 +435,12 @@ private fun confirmDeleteTaskFolder(project: Project, target: SessionActionTarge
) == Messages.YES
}
private fun showSetTaskFolderMetadataDialog(project: Project, target: SessionActionTarget.TaskFolder): TaskFolderMetadataEdit? {
private fun showSetTaskFolderMetadataDialog(project: Project, target: AgentTaskFolderActionTarget): TaskFolderMetadataEdit? {
val dialog = TaskFolderMetadataDialog(project, target)
return if (dialog.showAndGet()) dialog.metadataEdit() else null
}
private fun showDeleteTaskFolderMetadataDialog(project: Project, target: SessionActionTarget.TaskFolder): String? {
private fun showDeleteTaskFolderMetadataDialog(project: Project, target: AgentTaskFolderActionTarget): String? {
return Messages.showInputDialog(
project,
AgentSessionsBundle.message("toolwindow.task.folder.metadata.delete.dialog.message"),
@@ -515,7 +515,7 @@ private class CreateTaskFolderDialog(
private class TaskFolderMetadataDialog(
project: Project,
private val target: SessionActionTarget.TaskFolder,
private val target: AgentTaskFolderActionTarget,
) : DialogWrapper(project) {
private val keyCombo = ComboBox<@NlsSafe String>().apply {
model = DefaultComboBoxModel(taskFolderMetadataKeyOptions(target.metadata))
@@ -13,6 +13,8 @@ import com.intellij.agent.workbench.sessions.resolveAgentSessionLaunchProfileSel
import com.intellij.agent.workbench.sessions.service.AgentSessionLaunchService
import com.intellij.agent.workbench.sessions.state.AgentSessionUiPreferencesStateService
import com.intellij.agent.workbench.sessions.statistics.AgentWorkbenchEntryPoint
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolder
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolderService
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
@@ -23,8 +25,6 @@ import com.intellij.openapi.project.Project
import com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode
import com.intellij.platform.ai.agent.core.session.AgentSessionProvider
import com.intellij.platform.ai.agent.sessions.core.SessionActionTarget
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolder
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderService
import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProviderDescriptor
import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProviders
import com.intellij.platform.ai.agent.sessions.core.providers.builtInLaunchProfileId
@@ -204,7 +204,7 @@ internal fun buildTaskFolderAgentPrompt(
""".trimIndent()
}
private fun taskFolderAgentPathFromTarget(target: SessionActionTarget): String? {
private fun taskFolderAgentPathFromTarget(target: SessionActionTarget?): String? {
return when (target) {
is SessionActionTarget.Project -> target.path
is SessionActionTarget.Worktree -> target.path
@@ -4,6 +4,7 @@ package com.intellij.agent.workbench.sessions.toolwindow.actions
import com.intellij.platform.ai.agent.core.normalizeAgentWorkbenchPath
import com.intellij.platform.ai.agent.sessions.core.SessionActionTarget
import com.intellij.agent.workbench.sessions.model.ArchiveThreadTarget
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolderStatus
import com.intellij.agent.workbench.sessions.toolwindow.tree.SessionTreeId
import com.intellij.agent.workbench.sessions.toolwindow.tree.SessionTreeNode
import com.intellij.agent.workbench.sessions.toolwindow.tree.pathForMoreThreadsNode
@@ -11,7 +12,6 @@ import com.intellij.agent.workbench.sessions.toolwindow.tree.pathForTaskFolderNo
import com.intellij.agent.workbench.sessions.toolwindow.tree.pathForThreadNode
import com.intellij.agent.workbench.sessions.util.isAgentSessionNewSessionId
import com.intellij.openapi.project.Project
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderStatus
internal fun createAgentSessionsTreePopupActionContext(
project: Project,
@@ -23,10 +23,13 @@ internal fun createAgentSessionsTreePopupActionContext(
taskFolderArchiveTargets: List<ArchiveThreadTarget> = emptyList(),
newThreadActionAvailable: Boolean = true,
): AgentSessionsTreePopupActionContext? {
val target = resolveSessionActionTarget(nodeId, node) ?: return null
val target = resolveSessionActionTarget(nodeId, node)
val taskFolderTarget = resolveTaskFolderActionTarget(nodeId, node)
if (target == null && taskFolderTarget == null) return null
return AgentSessionsTreePopupActionContext(
project = project,
target = target,
taskFolderTarget = taskFolderTarget,
archiveTargets = archiveTargets,
unarchiveTargets = unarchiveTargets,
selectedThreadTargets = selectedThreadTargets,
@@ -56,16 +59,7 @@ internal fun resolveSessionActionTarget(nodeId: SessionTreeId, node: SessionTree
)
}
is SessionTreeNode.TaskFolder -> {
val path = pathForTaskFolderNode(nodeId) ?: return null
SessionActionTarget.TaskFolder(
path = normalizeAgentWorkbenchPath(path),
folderId = node.folder.id,
name = node.folder.name,
isDone = node.folder.status == AgentTaskFolderStatus.DONE,
metadata = node.folder.metadata,
)
}
is SessionTreeNode.TaskFolder -> null
is SessionTreeNode.Thread -> {
if (isAgentSessionNewSessionId(node.thread.id)) return null
@@ -125,3 +119,15 @@ internal fun resolveSessionActionTarget(nodeId: SessionTreeId, node: SessionTree
-> null
}
}
private fun resolveTaskFolderActionTarget(nodeId: SessionTreeId, node: SessionTreeNode): AgentTaskFolderActionTarget? {
if (node !is SessionTreeNode.TaskFolder) return null
val path = pathForTaskFolderNode(nodeId) ?: return null
return AgentTaskFolderActionTarget(
path = normalizeAgentWorkbenchPath(path),
folderId = node.folder.id,
name = node.folder.name,
isDone = node.folder.status == AgentTaskFolderStatus.DONE,
metadata = node.folder.metadata,
)
}
@@ -4,6 +4,7 @@ package com.intellij.agent.workbench.sessions.toolwindow.tree
import com.intellij.agent.workbench.chat.AgentChatOpenTabsPresentationState
import com.intellij.agent.workbench.chat.AgentChatPendingTabSnapshot
import com.intellij.platform.ai.agent.core.AgentThreadActivity
import com.intellij.platform.ai.agent.core.AgentThreadActivityReport
import com.intellij.platform.ai.agent.core.normalizeAgentWorkbenchPath
import com.intellij.platform.ai.agent.core.parseAgentThreadIdentity
import com.intellij.platform.ai.agent.core.session.AgentSessionProvider
@@ -83,7 +84,7 @@ private fun buildPendingThread(
title = AgentSessionsBundle.message("toolwindow.action.new.thread"),
updatedAt = pendingTab.pendingFirstInputAtMs ?: pendingTab.pendingCreatedAtMs ?: 0L,
archived = false,
activity = AgentThreadActivity.READY,
activityReport = AgentThreadActivityReport(AgentThreadActivity.READY),
provider = provider,
)
}
@@ -20,9 +20,9 @@ import com.intellij.agent.workbench.sessions.model.hasAnyProviderSnapshot
import com.intellij.agent.workbench.sessions.state.DEFAULT_VISIBLE_THREAD_COUNT
import com.intellij.agent.workbench.sessions.state.SessionTreeUiState
import com.intellij.agent.workbench.sessions.util.isAgentSessionNewSessionId
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolder
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderSnapshot
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderStatus
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolder
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolderSnapshot
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolderStatus
import com.intellij.openapi.util.NlsSafe
internal data class SessionTreeModel(
@@ -182,7 +182,7 @@ private fun AgentSessionsActivitySummary.rowsDebugText(): String {
return rows.take(MAX_ACTIVITY_DEBUG_ROWS).joinToString(prefix = "[", postfix = postfix) { bucketedRow ->
val row = bucketedRow.row
"${bucketedRow.bucket}:${row.path}:${row.thread.provider.value}:${row.thread.id}:" +
"activity=${row.thread.activity}:summaryActivity=${row.thread.summaryActivity}:updatedAt=${row.thread.updatedAt}"
"rowActivity=${row.thread.activityReport.rowActivity}:chromeActivity=${row.thread.activityReport.chromeActivity}:updatedAt=${row.thread.updatedAt}"
}
}
@@ -4,6 +4,7 @@ package com.intellij.agent.workbench.sessions.toolwindow.ui
// @spec community/plugins/agent-workbench/spec/sessions/agent-sessions-tree.spec.md
import com.intellij.platform.ai.agent.core.AgentThreadActivity
import com.intellij.platform.ai.agent.core.chromePresentationActivity
import com.intellij.platform.ai.agent.common.statusColor
import com.intellij.platform.ai.agent.common.statusMessageKey
import com.intellij.agent.workbench.sessions.AgentSessionsBundle
@@ -277,7 +278,7 @@ private class AgentSessionsActivityOpenThreadAction(
) : DumbAwareAction(
agentSessionsActivityPopupRowText(row, now),
null,
agentSessionThreadStatusIcon(row.thread.provider, row.thread.activity),
agentSessionThreadStatusIcon(row.thread.provider, row.thread.activityReport),
) {
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT
@@ -304,7 +305,7 @@ internal fun agentSessionsActivityPopupRowText(
val timeLabel = row.thread.updatedAt.takeIf { it > 0 }?.let { timestamp ->
formatRelativeTimeShort(timestamp, now)
} ?: AgentSessionsBundle.message("toolwindow.time.unknown")
val statusLabel = AgentSessionsBundle.message(row.thread.activity.statusMessageKey())
val statusLabel = AgentSessionsBundle.message(row.thread.activityReport.chromePresentationActivity().statusMessageKey())
return AgentSessionsBundle.message("toolwindow.activity.popup.row", title, statusLabel, row.locationLabel, timeLabel)
}
@@ -55,8 +55,8 @@ import com.intellij.ui.tree.StructureTreeModel
import com.intellij.ui.treeStructure.Tree
import com.intellij.util.ui.EdtInvocationManager
import com.intellij.util.ui.tree.TreeUtil
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderService
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderThreadAssignment
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolderService
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolderThreadAssignment
import java.awt.BorderLayout
import java.awt.Graphics
import java.util.Collections
@@ -399,8 +399,8 @@ internal class SessionTreeCellRenderer(
}
private fun threadCompositeIcon(treeNode: SessionTreeNode.Thread): Icon {
return providerIconProvider?.let { agentSessionThreadStatusIcon(it(treeNode.thread.provider), treeNode.thread.activity) }
?: agentSessionThreadStatusIcon(treeNode.thread.provider, treeNode.thread.activity)
return providerIconProvider?.let { agentSessionThreadStatusIcon(it(treeNode.thread.provider), treeNode.thread.activityReport.rowActivity) }
?: agentSessionThreadStatusIcon(treeNode.thread.provider, treeNode.thread.activityReport.rowActivity)
}
private fun projectCompositeIcon(project: AgentProjectSessions): Icon {
@@ -24,7 +24,7 @@ import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.platform.ai.agent.sessions.core.SessionActionTarget
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolder
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolder
import com.intellij.ui.hover.HoverListener
import com.intellij.ui.hover.TreeHoverListener
import com.intellij.ui.treeStructure.Tree
@@ -17,8 +17,8 @@ import com.intellij.agent.workbench.sessions.toolwindow.tree.buildSessionTreeMod
import com.intellij.agent.workbench.sessions.toolwindow.tree.diffSessionTreeModels
import com.intellij.agent.workbench.sessions.toolwindow.tree.overlayPendingAgentChatTabs
import com.intellij.agent.workbench.sessions.toolwindow.tree.resolveSelectedSessionTreeId
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderService
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderSnapshot
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolderService
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolderSnapshot
import com.intellij.openapi.application.UI
import com.intellij.openapi.components.serviceAsync
import com.intellij.ui.treeStructure.Tree
@@ -15,8 +15,8 @@ import com.intellij.ide.dnd.DnDEvent
import com.intellij.ide.dnd.DnDSupport
import com.intellij.ide.dnd.DnDTargetChecker
import com.intellij.openapi.Disposable
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolder
import com.intellij.platform.ai.agent.sessions.core.SessionActionTarget
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolder
import com.intellij.ui.awt.RelativeRectangle
import com.intellij.ui.treeStructure.Tree
import java.awt.Rectangle
@@ -36,15 +36,15 @@ internal fun buildSessionTreeThreadRowPresentation(
treeNode: SessionTreeNode.Thread,
now: Long,
): SessionTreeThreadRowPresentation {
val activityColor = treeNode.thread.activity.statusColor()
val rowActivity = treeNode.thread.activityReport.rowActivity
val activityColor = rowActivity.statusColor()
val timeLabel = treeNode.thread.updatedAt.takeIf { it > 0 }?.let { timestamp ->
formatRelativeTimeShort(timestamp, now)
} ?: AgentSessionsBundle.message("toolwindow.time.unknown")
val activity = treeNode.thread.activity
val statusLabel = threadActivityDisplayName(activity)
val statusLabel = threadActivityDisplayName(rowActivity)
val costLabel = treeNode.thread.cost.takeIf { AgentSessionCostPresentationSettings.isEnabled() }?.toDisplayLabel()
val trailingMetadataLabel = listOfNotNull(
statusLabel.takeIf { activity.hasVisibleTrailingStatus() },
statusLabel.takeIf { rowActivity.hasVisibleTrailingStatus() },
costLabel,
).joinToString(separator = " · ").takeIf { it.isNotEmpty() }
val originBranch = treeNode.thread.originBranch
@@ -322,7 +322,7 @@ class AgentSessionsActivitySummaryTest {
title = "Parent ready",
updatedAt = 100,
archived = false,
activity = AgentThreadActivity.READY,
activityReport = AgentThreadActivityReport(AgentThreadActivity.READY),
provider = AgentSessionProvider.from("codex"),
subAgents = listOf(AgentSubAgent(id = "child-done", name = "Child done", activity = AgentThreadActivity.UNREAD)),
),
@@ -348,7 +348,7 @@ class AgentSessionsActivitySummaryTest {
isOpen = true,
providerLoadStates = loadedProviderStates(AgentSessionProvider.from("codex")),
threads = listOf(
thread("sub-agent-done", AgentThreadActivity.UNREAD, 100, summaryActivity = null),
thread("sub-agent-done", AgentThreadActivity.UNREAD, 100, chromeActivity = null),
),
)
),
@@ -561,13 +561,13 @@ class AgentSessionsActivitySummaryTest {
assertThat(
tracker.collectNotifications(
summary(thread("done", AgentThreadActivity.UNREAD, 200, summaryActivity = null)),
summary(thread("done", AgentThreadActivity.UNREAD, 200, chromeActivity = null)),
isLoadedState = true,
)
).isEmpty()
assertThat(
tracker.collectNotifications(
summary(thread("done", AgentThreadActivity.UNREAD, 300, summaryActivity = AgentThreadActivity.UNREAD)),
summary(thread("done", AgentThreadActivity.UNREAD, 300, chromeActivity = AgentThreadActivity.UNREAD)),
isLoadedState = true,
)
).hasSize(1)
@@ -580,19 +580,19 @@ class AgentSessionsActivitySummaryTest {
assertThat(
tracker.collectNotifications(
summary(thread("needs-input", AgentThreadActivity.NEEDS_INPUT, 200, summaryActivity = AgentThreadActivity.READY)),
summary(thread("needs-input", AgentThreadActivity.NEEDS_INPUT, 200, chromeActivity = AgentThreadActivity.READY)),
isLoadedState = true,
)
).isEmpty()
assertThat(
tracker.collectNotifications(
summary(thread("needs-input", AgentThreadActivity.NEEDS_INPUT, 300, summaryActivity = null)),
summary(thread("needs-input", AgentThreadActivity.NEEDS_INPUT, 300, chromeActivity = null)),
isLoadedState = true,
)
).isEmpty()
assertThat(
tracker.collectNotifications(
summary(thread("needs-input", AgentThreadActivity.NEEDS_INPUT, 400, summaryActivity = AgentThreadActivity.NEEDS_INPUT)),
summary(thread("needs-input", AgentThreadActivity.NEEDS_INPUT, 400, chromeActivity = AgentThreadActivity.NEEDS_INPUT)),
isLoadedState = true,
)
).hasSize(1)
@@ -791,16 +791,15 @@ class AgentSessionsActivitySummaryTest {
activity: AgentThreadActivity,
updatedAt: Long,
title: String = id,
summaryActivity: AgentThreadActivity? = activity,
chromeActivity: AgentThreadActivity? = activity,
): AgentSessionThread {
return AgentSessionThread(
id = id,
title = title,
updatedAt = updatedAt,
archived = false,
activity = activity,
activityReport = AgentThreadActivityReport(rowActivity = activity, chromeActivity = chromeActivity),
provider = AgentSessionProvider.from("codex"),
summaryActivity = summaryActivity,
)
}
}
@@ -76,13 +76,14 @@ class AgentSessionsCodexActivityRenderingIntegrationTest {
service.state.value.projects.firstOrNull { it.path == PROJECT_PATH }
?.threads
?.singleOrNull()
?.activity == AgentThreadActivity.PROCESSING
?.activityReport
?.rowActivity == AgentThreadActivity.PROCESSING
}
val state = service.state.value
val project = state.projects.single { it.path == PROJECT_PATH }
val thread = project.threads.single()
assertThat(thread.activity).isEqualTo(AgentThreadActivity.PROCESSING)
assertThat(thread.activityReport.rowActivity).isEqualTo(AgentThreadActivity.PROCESSING)
val model = buildSessionTreeModel(
projects = state.projects,
@@ -92,7 +93,7 @@ class AgentSessionsCodexActivityRenderingIntegrationTest {
)
val threadId = SessionTreeId.Thread(project.path, AgentSessionProvider.from("codex"), thread.id)
val processingNode = model.entriesById.getValue(threadId).node as SessionTreeNode.Thread
assertThat(processingNode.thread.activity).isEqualTo(AgentThreadActivity.PROCESSING)
assertThat(processingNode.thread.activityReport.rowActivity).isEqualTo(AgentThreadActivity.PROCESSING)
val tree = createTree()
val processingRenderer = createRenderer { id -> model.entriesById[id]?.node }
@@ -118,15 +119,14 @@ class AgentSessionsCodexActivityRenderingIntegrationTest {
}
@Test
fun treeRowRendersActualActivityWhenSummaryActivityDoesNotContribute() {
fun treeRowRendersActualActivityWhenChromeActivityDoesNotContribute() {
val thread = AgentSessionThread(
id = "sub-agent-only",
title = "Sub-agent only",
updatedAt = 1_000L,
archived = false,
activity = AgentThreadActivity.UNREAD,
activityReport = AgentThreadActivityReport(rowActivity = AgentThreadActivity.UNREAD, chromeActivity = null),
provider = AgentSessionProvider.from("codex"),
summaryActivity = null,
)
val project = AgentProjectSessions(
path = PROJECT_PATH,
@@ -143,8 +143,8 @@ class AgentSessionsCodexActivityRenderingIntegrationTest {
)
val threadId = SessionTreeId.Thread(project.path, AgentSessionProvider.from("codex"), thread.id)
val unreadNode = model.entriesById.getValue(threadId).node as SessionTreeNode.Thread
assertThat(unreadNode.thread.activity).isEqualTo(AgentThreadActivity.UNREAD)
assertThat(unreadNode.thread.summaryActivity).isNull()
assertThat(unreadNode.thread.activityReport.rowActivity).isEqualTo(AgentThreadActivity.UNREAD)
assertThat(unreadNode.thread.activityReport.chromeActivity).isNull()
val tree = createTree()
val unreadRenderer = createRenderer { id -> model.entriesById[id]?.node }
@@ -181,8 +181,7 @@ class AgentSessionsCodexActivityRenderingIntegrationTest {
title = "Thread 1",
updatedAt = 1_000L,
provider = AgentSessionProvider.from("codex"),
activity = AgentThreadActivity.PROCESSING,
summaryActivity = null,
activityReport = AgentThreadActivityReport(rowActivity = AgentThreadActivity.PROCESSING, chromeActivity = null),
)
)
}
@@ -2,6 +2,7 @@
package com.intellij.agent.workbench.sessions.toolwindow
import com.intellij.platform.ai.agent.core.AgentThreadActivity
import com.intellij.platform.ai.agent.core.AgentThreadActivityReport
import com.intellij.agent.workbench.ui.AgentWorkbenchActionIds
import com.intellij.platform.ai.agent.core.session.AgentSessionProvider
import com.intellij.platform.ai.agent.core.session.AgentSessionThread
@@ -326,9 +327,8 @@ class AgentSessionsMainToolbarActivityGroupTest {
title = id,
updatedAt = updatedAt,
archived = false,
activity = activity,
activityReport = AgentThreadActivityReport(rowActivity = activity, chromeActivity = activity),
provider = AgentSessionProvider.from("codex"),
summaryActivity = activity,
)
}
}
@@ -756,7 +756,7 @@ class AgentSessionsSwingTreeCellRendererTest {
title = "How much time",
updatedAt = 0L,
archived = false,
activity = AgentThreadActivity.UNREAD,
activityReport = AgentThreadActivityReport(AgentThreadActivity.UNREAD),
)
val providerBaseIcon = EmptyIcon.create(12, 12)
val threadId = SessionTreeId.Thread(project.path, thread.provider, thread.id)
@@ -795,7 +795,7 @@ class AgentSessionsSwingTreeCellRendererTest {
title = "Need input",
updatedAt = 14L * 24L * 60L * 60L * 1000L,
archived = false,
activity = AgentThreadActivity.NEEDS_INPUT,
activityReport = AgentThreadActivityReport(AgentThreadActivity.NEEDS_INPUT),
)
val threadId = SessionTreeId.Thread(project.path, thread.provider, thread.id)
val renderer = SessionTreeCellRenderer(
@@ -830,7 +830,7 @@ class AgentSessionsSwingTreeCellRendererTest {
title = "Processing",
updatedAt = 14L * 24L * 60L * 60L * 1000L,
archived = false,
activity = AgentThreadActivity.PROCESSING,
activityReport = AgentThreadActivityReport(AgentThreadActivity.PROCESSING),
)
val threadId = SessionTreeId.Thread(project.path, thread.provider, thread.id)
val renderer = SessionTreeCellRenderer(
@@ -867,7 +867,7 @@ class AgentSessionsSwingTreeCellRendererTest {
title = "Ready thread",
updatedAt = 14L * 24L * 60L * 60L * 1000L,
archived = false,
activity = AgentThreadActivity.READY,
activityReport = AgentThreadActivityReport(AgentThreadActivity.READY),
)
val threadId = SessionTreeId.Thread(project.path, thread.provider, thread.id)
val renderer = SessionTreeCellRenderer(
@@ -895,7 +895,7 @@ class AgentSessionsSwingTreeCellRendererTest {
title = "How much time",
updatedAt = 0L,
archived = false,
activity = AgentThreadActivity.READY,
activityReport = AgentThreadActivityReport(AgentThreadActivity.READY),
)
val readyThreadId = SessionTreeId.Thread(project.path, readyThread.provider, readyThread.id)
val renderer = SessionTreeCellRenderer(
@@ -993,7 +993,7 @@ class AgentSessionsSwingTreeCellRendererTest {
title = "Thread 1",
updatedAt = 0L,
archived = false,
activity = AgentThreadActivity.READY,
activityReport = AgentThreadActivityReport(AgentThreadActivity.READY),
subAgents = listOf(
AgentSubAgent(id = "sub-ready", name = "Ready sub-agent"),
AgentSubAgent(id = "sub-done", name = "Done sub-agent", activity = AgentThreadActivity.UNREAD),
@@ -1154,7 +1154,7 @@ class AgentSessionsSwingTreeCellRendererTest {
title = "Status thread",
updatedAt = 14L * 24L * 60L * 60L * 1000L,
archived = false,
activity = activity,
activityReport = AgentThreadActivityReport(activity),
)
val tooltip = buildSessionTreeThreadTooltipHtml(
treeNode = SessionTreeNode.Thread(project, thread),
@@ -1394,7 +1394,7 @@ class AgentSessionsSwingTreeCellRendererTest {
title = "Needs input",
updatedAt = 0L,
archived = false,
activity = AgentThreadActivity.NEEDS_INPUT,
activityReport = AgentThreadActivityReport(AgentThreadActivity.NEEDS_INPUT),
cost = AgentSessionCost(amountUsd = BigDecimal("0.42"), kind = AgentSessionCostKind.ESTIMATED),
)
val project = AgentProjectSessions(path = "/work/project-a", name = "Project A", isOpen = true)
@@ -1418,7 +1418,7 @@ class AgentSessionsSwingTreeCellRendererTest {
title = "Done",
updatedAt = 0L,
archived = false,
activity = AgentThreadActivity.UNREAD,
activityReport = AgentThreadActivityReport(AgentThreadActivity.UNREAD),
cost = AgentSessionCost(amountUsd = BigDecimal("0.42"), kind = AgentSessionCostKind.ESTIMATED),
)
val project = AgentProjectSessions(path = "/work/project-a", name = "Project A", isOpen = true)
@@ -2,6 +2,7 @@
package com.intellij.agent.workbench.sessions.toolwindow
import com.intellij.platform.ai.agent.core.AgentThreadActivity
import com.intellij.platform.ai.agent.core.AgentThreadActivityReport
import com.intellij.platform.ai.agent.core.session.AgentSessionProvider
import com.intellij.platform.ai.agent.core.session.AgentSessionThread
import com.intellij.agent.workbench.sessions.model.ProjectBuildSystemBadge
@@ -173,7 +174,7 @@ class AgentSessionsTreeModelDiffTest {
title = "Thread 1",
updatedAt = 100,
archived = false,
activity = AgentThreadActivity.READY,
activityReport = AgentThreadActivityReport(AgentThreadActivity.READY),
provider = AgentSessionProvider.from("codex"),
)
),
@@ -196,7 +197,7 @@ class AgentSessionsTreeModelDiffTest {
title = "Thread 1",
updatedAt = 100,
archived = false,
activity = AgentThreadActivity.PROCESSING,
activityReport = AgentThreadActivityReport(AgentThreadActivity.PROCESSING),
provider = AgentSessionProvider.from("codex"),
)
),
@@ -15,6 +15,7 @@ import com.intellij.platform.ai.agent.sessions.core.providers.builtInLaunchProfi
import com.intellij.agent.workbench.sessions.statistics.AgentWorkbenchEntryPoint
import com.intellij.agent.workbench.sessions.model.ArchiveThreadTarget
import com.intellij.agent.workbench.sessions.service.AgentSessionProviderAvailabilityService
import com.intellij.agent.workbench.sessions.toolwindow.actions.AgentTaskFolderActionTarget
import com.intellij.agent.workbench.sessions.toolwindow.actions.AgentSessionsTreePopupActionContext
import com.intellij.agent.workbench.sessions.toolwindow.actions.AgentSessionsTreePopupArchiveThreadAction
import com.intellij.agent.workbench.sessions.toolwindow.actions.AgentSessionsTreePopupCopyThreadIdAction
@@ -54,9 +55,9 @@ import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.agent.workbench.sessions.service.AgentSessionArchiveRequestResult
import com.intellij.platform.ai.agent.sessions.core.SessionActionTarget
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolder
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderStatus
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderThreadAssignment
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolder
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolderStatus
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolderThreadAssignment
import com.intellij.testFramework.LightVirtualFile
import com.intellij.testFramework.TestActionEvent
import com.intellij.testFramework.junit5.TestApplication
@@ -807,7 +808,7 @@ class AgentSessionsTreePopupActionsTest {
@Test
fun markTaskFolderDoneArchivesAssignedTargetsBeforeChangingStatus() {
val provider = AgentSessionProvider.from("codex")
val folderTarget = SessionActionTarget.TaskFolder(
val folderTarget = AgentTaskFolderActionTarget(
path = "/work/project-a",
folderId = "folder1",
name = "Research",
@@ -816,7 +817,7 @@ class AgentSessionsTreePopupActionsTest {
val loadedArchiveTarget = ArchiveThreadTarget.Thread(path = "/work/project-a", provider = provider, threadId = "loaded-thread")
val unloadedArchiveTarget = ArchiveThreadTarget.Thread(path = "/work/project-a", provider = provider, threadId = "unloaded-thread")
var archivedTargets: List<ArchiveThreadTarget>? = null
var doneTarget: SessionActionTarget.TaskFolder? = null
var doneTarget: AgentTaskFolderActionTarget? = null
val action = AgentSessionsTreePopupMarkTaskFolderDoneAction(
resolveContext = { event -> resolveAgentSessionsTreePopupActionContext(event) },
canArchiveProvider = { true },
@@ -828,7 +829,7 @@ class AgentSessionsTreePopupActionsTest {
)
val context = AgentSessionsTreePopupActionContext(
project = ProjectManager.getInstance().defaultProject,
target = folderTarget,
taskFolderTarget = folderTarget,
archiveTargets = emptyList(),
taskFolderArchiveTargets = listOf(loadedArchiveTarget, unloadedArchiveTarget),
)
@@ -865,13 +866,13 @@ class AgentSessionsTreePopupActionsTest {
@Test
fun markTaskFolderDoneKeepsFolderInProgressWhenAssignedArchiveIsPartial() {
val provider = AgentSessionProvider.from("codex")
val folderTarget = SessionActionTarget.TaskFolder(
val folderTarget = AgentTaskFolderActionTarget(
path = "/work/project-a",
folderId = "folder1",
name = "Research",
isDone = false,
)
var doneTarget: SessionActionTarget.TaskFolder? = null
var doneTarget: AgentTaskFolderActionTarget? = null
val action = AgentSessionsTreePopupMarkTaskFolderDoneAction(
resolveContext = { event -> resolveAgentSessionsTreePopupActionContext(event) },
canArchiveProvider = { true },
@@ -882,7 +883,7 @@ class AgentSessionsTreePopupActionsTest {
)
val context = AgentSessionsTreePopupActionContext(
project = ProjectManager.getInstance().defaultProject,
target = folderTarget,
taskFolderTarget = folderTarget,
archiveTargets = emptyList(),
taskFolderArchiveTargets = listOf(
ArchiveThreadTarget.Thread(path = "/work/project-a", provider = provider, threadId = "loaded-thread"),
@@ -907,7 +908,7 @@ class AgentSessionsTreePopupActionsTest {
)
val context = AgentSessionsTreePopupActionContext(
project = ProjectManager.getInstance().defaultProject,
target = SessionActionTarget.TaskFolder(
taskFolderTarget = AgentTaskFolderActionTarget(
path = "/work/project-a",
folderId = "folder1",
name = "Research",
@@ -17,10 +17,10 @@ import com.intellij.agent.workbench.sessions.toolwindow.tree.buildSessionTreeMod
import com.intellij.agent.workbench.sessions.toolwindow.tree.overlayPendingAgentChatTabs
import com.intellij.agent.workbench.sessions.toolwindow.tree.sessionTreeNodeSearchText
import com.intellij.agent.workbench.sessions.toolwindow.ui.SessionTreeStrictSubstringComparator
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolder
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderSnapshot
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderStatus
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderThreadAssignment
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolder
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolderSnapshot
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolderStatus
import com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolderThreadAssignment
import com.intellij.testFramework.junit5.TestApplication
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
@@ -483,7 +483,7 @@ class AgentSessionsTreeSnapshotTest {
assertThat(pendingThread.id).isEqualTo("new-pending")
assertThat(pendingThread.title).isEqualTo(AgentSessionsBundle.message("toolwindow.action.new.thread"))
assertThat(pendingThread.updatedAt).isEqualTo(700L)
assertThat(pendingThread.activity).isEqualTo(AgentThreadActivity.READY)
assertThat(pendingThread.activityReport.rowActivity).isEqualTo(AgentThreadActivity.READY)
assertThat(pendingThread.provider).isEqualTo(AgentSessionProvider.from("codex"))
}
@@ -5,6 +5,7 @@ import com.intellij.agent.workbench.chat.AgentChatOpenTabsPresentationState
import com.intellij.agent.workbench.chat.AgentChatPendingTabSnapshot
import com.intellij.agent.workbench.chat.AgentChatTabSelection
import com.intellij.platform.ai.agent.core.AgentThreadActivity
import com.intellij.platform.ai.agent.core.AgentThreadActivityReport
import com.intellij.platform.ai.agent.core.buildAgentThreadIdentity
import com.intellij.platform.ai.agent.core.session.AgentSessionProvider
import com.intellij.platform.ai.agent.core.session.AgentSessionThread
@@ -532,7 +533,7 @@ private fun stateWithThread(
updatedAt = updatedAt,
archived = false,
provider = AgentSessionProvider.from("codex"),
activity = activity,
activityReport = AgentThreadActivityReport(activity),
)
),
)
@@ -575,7 +576,7 @@ private fun thread(
updatedAt = updatedAt,
archived = false,
provider = AgentSessionProvider.from("codex"),
activity = activity,
activityReport = AgentThreadActivityReport(activity),
)
}
@@ -205,10 +205,6 @@ settings.agent.workbench.session.cost.description=When enabled, Agent Threads sh
settings.agent.workbench.jbcentral.quota.status.bar.widget=Show JBCentral quota in status bar
settings.agent.workbench.jbcentral.quota.status.bar.widget.description=When enabled, Agent Workbench shows the remaining JBCentral quota in the IDE status bar.
settings.agent.workbench.providers.name=Providers
settings.agent.workbench.provider.pi.omlx.models=Use local oMLX models
settings.agent.workbench.provider.pi.omlx.models.description=When enabled, Agent Workbench discovers local oMLX models and registers the selected model for Pi sessions.
settings.agent.workbench.provider.pi.jbcentral.models=Use JetBrains Central models
settings.agent.workbench.provider.pi.jbcentral.models.description=When enabled, Agent Workbench detects JetBrains Central wiring and registers it for Pi sessions.
action.AgentWorkbenchSessions.ToggleSessionCost.text=Show Session Cost
action.AgentWorkbenchSessions.ToggleSessionCost.description=Show per-session cost in Agent Threads
toolwindow.session.cost.hint.title=Show session cost in Agent Threads?
@@ -449,8 +449,8 @@ private fun markThreadAsRead(
changed = true
thread.copy(
activityReport = AgentThreadActivityReport(
rowActivity = thread.activity.takeUnless { it == AgentThreadActivity.UNREAD } ?: AgentThreadActivity.READY,
chromeActivity = thread.summaryActivity?.takeUnless { it == AgentThreadActivity.UNREAD } ?: AgentThreadActivity.READY,
rowActivity = thread.activityReport.rowActivity.takeUnless { it == AgentThreadActivity.UNREAD } ?: AgentThreadActivity.READY,
chromeActivity = thread.activityReport.chromeActivity?.takeUnless { it == AgentThreadActivity.UNREAD } ?: AgentThreadActivity.READY,
),
)
}
@@ -524,7 +524,7 @@ class AgentSessionLaunchService internal constructor(
promptLaunchResolved?.invoke(AgentPromptLaunchResult.failure(AgentPromptLaunchError.TARGET_THREAD_NOT_FOUND))
return@launchDropAction
}
if (effectiveInitialMessagePlan?.isBlockedForExistingThreadPlanMode(refreshedThread.activity) == true) {
if (effectiveInitialMessagePlan?.isBlockedForExistingThreadPlanMode(refreshedThread.activityReport.rowActivity) == true) {
promptLaunchResolved?.invoke(AgentPromptLaunchResult.failure(AgentPromptLaunchError.TARGET_THREAD_BUSY_FOR_PLAN_MODE))
return@launchDropAction
}
@@ -648,7 +648,7 @@ class AgentSessionLaunchService internal constructor(
provider = thread.provider,
threadId = thread.id,
title = thread.title,
activity = thread.activity,
activity = thread.activityReport.rowActivity,
updatedAt = thread.updatedAt,
)
val matchResult = PendingAgentChatTabMatcher.match(
@@ -1407,7 +1407,7 @@ class AgentSessionLaunchService internal constructor(
?: return@run reportPromptLaunchResolved(AgentPromptLaunchResult.failure(AgentPromptLaunchError.TARGET_THREAD_NOT_FOUND))
val effectiveInitialMessageRequest = request.initialMessageRequest
val initialMessagePlan = bridge.buildInitialMessagePlan(effectiveInitialMessageRequest)
if (initialMessagePlan.isBlockedForExistingThreadPlanMode(targetThread.activity)) {
if (initialMessagePlan.isBlockedForExistingThreadPlanMode(targetThread.activityReport.rowActivity)) {
return@run reportPromptLaunchResolved(AgentPromptLaunchResult.failure(AgentPromptLaunchError.TARGET_THREAD_BUSY_FOR_PLAN_MODE))
}
uiPreferencesState.updateProviderOptionsOnLaunch(
@@ -2252,7 +2252,7 @@ private suspend fun openChatInProject(
threadId = chatOpenPlan.runtimeThreadId,
threadTitle = chatOpenPlan.threadTitle,
subAgentId = chatOpenPlan.subAgentId,
threadActivity = thread.activity,
threadActivity = thread.activityReport.rowActivity,
launchMode = serializeAgentChatLaunchMode(launchMode),
launchProfileId = launchProfileId,
initialMessageDispatchPlan = effectiveInitialMessageDispatchPlan,
@@ -69,7 +69,7 @@ class AgentSessionReadService private constructor(
threadIdentity = buildAgentSessionIdentity(provider = thread.provider, sessionId = thread.id),
threadId = thread.id,
threadTitle = thread.title,
threadActivity = thread.activity,
threadActivity = thread.activityReport.rowActivity,
threadUpdatedAt = thread.updatedAt,
)
}
@@ -838,8 +838,8 @@ private fun applyThreadPresentationHintsForPath(
LOG.debug {
"Applying ${provider.value} presentation hint path=$path threadId=${thread.id} " +
"titleChanged=${resolvedUpdate.title != thread.title} " +
"activity=${thread.activity}->${resolvedUpdate.activityReport.rowActivity} " +
"summaryActivity=${thread.summaryActivity}->${resolvedUpdate.activityReport.chromeActivity} " +
"rowActivity=${thread.activityReport.rowActivity}->${resolvedUpdate.activityReport.rowActivity} " +
"chromeActivity=${thread.activityReport.chromeActivity}->${resolvedUpdate.activityReport.chromeActivity} " +
"updatedAt=${thread.updatedAt}->${resolvedUpdate.updatedAt}"
}
presentationUpdates += AgentSessionThreadPresentationPatchUpdate(
@@ -179,7 +179,6 @@ class AgentSessionRenameService internal constructor(
provider = target.provider,
threadId = target.threadId,
title = normalizedRequestedName,
activity = null,
activityReport = activityReport,
)
}
@@ -196,7 +196,7 @@ internal class AgentSessionThreadRebindSupport(
provider = provider,
threadId = thread.id,
title = thread.title,
activity = thread.activity,
activity = thread.activityReport.rowActivity,
updatedAt = thread.updatedAt,
)
)
@@ -5,5 +5,5 @@ import com.intellij.platform.ai.agent.core.AgentThreadActivity
import com.intellij.platform.ai.agent.core.session.AgentSessionThread
internal fun AgentSessionThread.hasUnreadActivitySignal(): Boolean {
return activity == AgentThreadActivity.UNREAD || summaryActivity == AgentThreadActivity.UNREAD
return activityReport.rowActivity == AgentThreadActivity.UNREAD || activityReport.chromeActivity == AgentThreadActivity.UNREAD
}
@@ -240,7 +240,7 @@ private fun shouldReuseCachedCost(
if (cacheEntry.updatedAt != visibleThread.updatedAt) {
return false
}
if (!visibleThread.activity.isWorking) {
if (!visibleThread.rowActivity.isWorking) {
return true
}
return nowMs - cacheEntry.refreshedAtMs < workingThreadCostCacheTtlMs
@@ -265,8 +265,8 @@ private data class VisibleThreadSnapshot(
val cost: AgentSessionCost?
get() = thread.cost
val activity: AgentThreadActivity
get() = thread.activity
val rowActivity: AgentThreadActivity
get() = thread.activityReport.rowActivity
val cacheKey: ThreadCacheKey
get() = ThreadCacheKey(path = path, provider = provider, threadId = threadId)
@@ -255,7 +255,7 @@ private fun createPowerSaveModeFlow(serviceScope: CoroutineScope): StateFlow<Boo
private fun AgentSessionsState.hasSleepPreventingWork(): Boolean {
return projects.any { project ->
project.threads.any { thread -> thread.activity.isWorking } ||
project.worktrees.any { worktree -> worktree.threads.any { thread -> thread.activity.isWorking } }
project.threads.any { thread -> thread.activityReport.rowActivity.isWorking } ||
project.worktrees.any { worktree -> worktree.threads.any { thread -> thread.activityReport.rowActivity.isWorking } }
}
}
@@ -2,6 +2,7 @@
package com.intellij.agent.workbench.sessions.state
import com.intellij.platform.ai.agent.core.AgentThreadActivity
import com.intellij.platform.ai.agent.core.AgentThreadActivityReport
import com.intellij.platform.ai.agent.core.normalizeAgentWorkbenchPath
import com.intellij.platform.ai.agent.core.session.AgentSessionCost
import com.intellij.platform.ai.agent.core.session.AgentSessionCostKind
@@ -202,7 +203,10 @@ private fun AgentSessionWarmStateService.WarmPathSnapshotState.toSnapshot(): Age
title = threadDisplayTitle(threadId = thread.id, title = thread.title),
updatedAt = thread.updatedAt,
archived = false,
activity = parseWarmStateThreadActivity(thread.activity),
activityReport = AgentThreadActivityReport(
rowActivity = parseWarmStateThreadActivity(thread.activity),
chromeActivity = parseWarmStateThreadSummaryActivity(thread.summaryActivity),
),
provider = provider,
subAgents = thread.subAgents.map { subAgent ->
AgentSubAgent(
@@ -212,7 +216,6 @@ private fun AgentSessionWarmStateService.WarmPathSnapshotState.toSnapshot(): Age
)
},
originBranch = thread.originBranch,
summaryActivity = parseWarmStateThreadSummaryActivity(thread.summaryActivity),
cost = thread.cost?.toCost(),
)
},
@@ -231,13 +234,13 @@ private fun AgentSessionWarmPathSnapshot.toState(): AgentSessionWarmStateService
id = thread.id,
title = thread.title,
updatedAt = thread.updatedAt,
activity = thread.activity.name,
activity = thread.activityReport.rowActivity.name,
provider = thread.provider.value,
subAgents = thread.subAgents.map { subAgent ->
AgentSessionWarmStateService.WarmSubAgentState(id = subAgent.id, name = subAgent.name, activity = subAgent.activity.name)
},
originBranch = thread.originBranch,
summaryActivity = thread.summaryActivity?.name,
summaryActivity = thread.activityReport.chromeActivity?.name,
cost = thread.cost?.toState(),
)
},
@@ -357,7 +357,8 @@ fun thread(
provider: AgentSessionProvider,
title: String = id,
activity: AgentThreadActivity = AgentThreadActivity.READY,
summaryActivity: AgentThreadActivity? = activity,
chromeActivity: AgentThreadActivity? = activity,
activityReport: AgentThreadActivityReport = AgentThreadActivityReport(rowActivity = activity, chromeActivity = chromeActivity),
subAgents: List<AgentSubAgent> = emptyList(),
cost: AgentSessionCost? = null,
): AgentSessionThread {
@@ -366,9 +367,8 @@ fun thread(
title = title,
updatedAt = updatedAt,
archived = false,
activityReport = activityReport,
provider = provider,
activity = activity,
summaryActivity = summaryActivity,
subAgents = subAgents,
cost = cost,
)

Some files were not shown because too many files have changed in this diff Show More