From 17519f0daa768d1d5a9491ebd74485ca0ef9c3b1 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Mon, 29 Jun 2026 12:07:04 +0200 Subject: [PATCH] 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 --- .idea/modules.xml | 2 + build/bazel-generated-file-list.txt | 1 + .../chat/src/AgentChatVirtualFile.kt | 2 +- .../testSrc/AgentChatEditorServiceTest.kt | 58 +--- .../AgentChatFileEditorLifecycleTest.kt | 3 +- .../AgentChatFileEditorProviderTest.kt | 24 +- .../AgentChatOpenTopLevelDispatchTest.kt | 3 +- .../lib-agent/core/api-dump.txt | 3 - .../core/src/session/AgentSessionModels.kt | 31 +- .../sessions/src/ClaudeSessionSource.kt | 12 +- .../testSrc/ClaudeSessionSourceTest.kt | 42 +-- .../codex/sessions/src/CodexSessionSource.kt | 7 +- ...odexSessionSourceRolloutIntegrationTest.kt | 10 +- .../testSrc/CodexSessionSourceTest.kt | 8 +- .../junie/sessions/src/JunieSessionSource.kt | 5 +- .../testSrc/JunieSessionSourceTest.kt | 16 +- .../sessions/src/OpenCodeSessionSource.kt | 3 +- .../testSrc/OpenCodeSessionSourceTest.kt | 2 +- .../providers/pi/sessions/BUILD.bazel | 6 +- .../pi/sessions/PiControlProtocol.md | 8 +- ...intellij.platform.ai.agent.pi.sessions.iml | 1 - ...ij.platform.ai.agent.pi.sessions.tests.iml | 2 + ...intellij.platform.ai.agent.pi.sessions.xml | 4 + .../pi-extension/agent-workbench-extension.ts | 3 +- .../resources/pi-extension/control.ts | 287 +----------------- .../resources/pi-extension/taskFolders.ts | 273 +++++++++++++++++ .../sessions/src/PiAgentSessionProviderId.kt | 4 +- .../sessions/src/PiControlRequestHandler.kt | 23 ++ .../sessions/src/PiExtensionControlBridge.kt | 20 +- .../src/PiExtensionControlProtocol.kt | 54 +++- .../pi/sessions/src/PiSessionSource.kt | 13 +- .../pi/sessions/src/PiThemeSupport.kt | 1 + .../PiAgentSessionProviderDescriptorTest.kt | 4 +- .../PiExtensionControlWebSocketHandlerTest.kt | 7 +- .../sessions/testSrc/PiSessionSourceTest.kt | 37 ++- .../pi/sessions/testSrc/PiThemeSupportTest.kt | 10 +- .../src/TerminalSessionStateService.kt | 2 - .../lib-agent/sessions-core/api-dump.txt | 25 +- .../AgentSessionThreadPresentationModel.kt | 29 +- .../sessions-core/src/SessionActionTarget.kt | 8 - plugins/agent-workbench/pi/awb/BUILD.bazel | 8 +- .../awb/intellij.agent.workbench.pi.awb.iml | 4 +- .../intellij.agent.workbench.pi.awb.xml | 4 +- .../messages/PiSessionsBundle.properties | 4 + .../awb/src/PiProviderSettingsContributor.kt | 11 +- .../pi/awb/src/PiSessionsBundle.kt | 18 ++ .../awb}/src/PiTaskFolderControlHandler.kt | 117 ++++--- plugins/agent-workbench/plugin-content.yaml | 1 + plugins/agent-workbench/plugin/BUILD.bazel | 2 + .../intellij.agent.workbench.plugin.tests.iml | 1 + .../plugin/plugin-content.yaml | 3 + .../plugin/resources/META-INF/plugin.xml | 1 + .../src/AgentPromptExistingTaskController.kt | 2 +- .../AgentPromptExistingTaskControllerTest.kt | 3 +- ...AgentPromptPaletteSessionControllerTest.kt | 3 +- .../AgentPromptPaletteSubmitControllerTest.kt | 3 +- .../sessions-task-folders/BUILD.bazel | 136 +++++++++ ....agent.workbench.sessions.task.folders.iml | 40 +++ ....workbench.sessions.task.folders.tests.iml | 45 +++ ....agent.workbench.sessions.task.folders.xml | 7 + .../src}/AgentTaskFolders.kt | 2 +- .../testSrc/AgentTaskFolderServiceTest.kt | 4 +- .../sessions-toolwindow/BUILD.bazel | 4 + ...ij.agent.workbench.sessions.toolwindow.iml | 1 + ...nt.workbench.sessions.toolwindow.tests.iml | 1 + ...ij.agent.workbench.sessions.toolwindow.xml | 1 + .../actions/AgentSessionsTreePopupActions.kt | 15 +- ...AgentSessionsTreePopupTaskFolderActions.kt | 56 ++-- ...SessionsTreePopupTaskFolderAgentActions.kt | 6 +- .../src/actions/SessionTreeActionTargets.kt | 30 +- .../src/tree/PendingAgentChatTreeOverlay.kt | 3 +- .../src/tree/SessionTree.kt | 6 +- .../src/ui/AgentSessionsActivityService.kt | 2 +- .../src/ui/AgentSessionsTitleActions.kt | 5 +- .../src/ui/AgentSessionsToolWindow.kt | 4 +- .../src/ui/AgentSessionsTreeCellRenderer.kt | 4 +- .../AgentSessionsTreeInteractionController.kt | 2 +- .../ui/AgentSessionsTreeStateController.kt | 4 +- .../AgentSessionsTreeTaskFolderDnDSupport.kt | 2 +- .../src/ui/SessionTreeThreadPresentation.kt | 8 +- .../AgentSessionsActivitySummaryTest.kt | 19 +- ...nsCodexActivityRenderingIntegrationTest.kt | 19 +- ...entSessionsMainToolbarActivityGroupTest.kt | 4 +- .../AgentSessionsSwingTreeCellRendererTest.kt | 18 +- .../testSrc/AgentSessionsTreeModelDiffTest.kt | 5 +- .../AgentSessionsTreePopupActionsTest.kt | 21 +- .../testSrc/AgentSessionsTreeSnapshotTest.kt | 10 +- .../AgentSessionsTreeStateControllerTest.kt | 5 +- .../messages/AgentSessionsBundle.properties | 4 - .../service/AgentSessionContentRepository.kt | 4 +- .../src/service/AgentSessionLaunchService.kt | 8 +- .../src/service/AgentSessionReadService.kt | 2 +- .../service/AgentSessionRefreshCoordinator.kt | 4 +- .../src/service/AgentSessionRenameService.kt | 1 - .../AgentSessionThreadRebindSupport.kt | 2 +- .../AgentSessionUnreadActivitySupport.kt | 2 +- ...AgentSessionVisibleCostHydrationSupport.kt | 6 +- .../AgentSessionSleepPreventionService.kt | 4 +- .../src/state/AgentSessionWarmStateService.kt | 11 +- .../AgentSessionIntegrationTestSupport.kt | 6 +- .../AgentSessionRefreshCoordinatorTest.kt | 49 +-- ...entSessionRefreshServiceIntegrationTest.kt | 30 +- .../testSrc/AgentSessionRenameServiceTest.kt | 2 +- .../AgentSessionSleepPreventionServiceTest.kt | 37 ++- ...AgentSessionThreadPresentationModelTest.kt | 21 +- .../AgentSessionWarmStateServiceTest.kt | 14 +- .../model/AgentSessionThreadOrderingTest.kt | 3 +- .../agent-sessions-codex-activity.spec.md | 2 +- .../spec/sessions/agent-sessions-pi.spec.md | 19 +- .../sessions/agent-sessions-refresh.spec.md | 4 +- .../agent-sessions-sleep-prevention.spec.md | 6 +- .../spec/sessions/agent-sessions-tree.spec.md | 2 +- .../spec/sessions/agent-sessions.spec.md | 2 +- .../spec/sessions/agent-task-folders.spec.md | 12 +- 114 files changed, 1158 insertions(+), 826 deletions(-) create mode 100644 plugins/agent-workbench/lib-agent/providers/pi/sessions/resources/pi-extension/taskFolders.ts create mode 100644 plugins/agent-workbench/lib-agent/providers/pi/sessions/src/PiControlRequestHandler.kt create mode 100644 plugins/agent-workbench/pi/awb/resources/messages/PiSessionsBundle.properties create mode 100644 plugins/agent-workbench/pi/awb/src/PiSessionsBundle.kt rename plugins/agent-workbench/{lib-agent/providers/pi/sessions => pi/awb}/src/PiTaskFolderControlHandler.kt (80%) create mode 100644 plugins/agent-workbench/sessions-task-folders/BUILD.bazel create mode 100644 plugins/agent-workbench/sessions-task-folders/intellij.agent.workbench.sessions.task.folders.iml create mode 100644 plugins/agent-workbench/sessions-task-folders/intellij.agent.workbench.sessions.task.folders.tests.iml create mode 100644 plugins/agent-workbench/sessions-task-folders/resources/intellij.agent.workbench.sessions.task.folders.xml rename plugins/agent-workbench/{lib-agent/sessions-core/src/folders => sessions-task-folders/src}/AgentTaskFolders.kt (99%) rename plugins/agent-workbench/{sessions => sessions-task-folders}/testSrc/AgentTaskFolderServiceTest.kt (96%) diff --git a/.idea/modules.xml b/.idea/modules.xml index 9136f5753b33..6d7eea6a20ba 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -81,6 +81,8 @@ + + diff --git a/build/bazel-generated-file-list.txt b/build/bazel-generated-file-list.txt index 6a69efd54263..21df8cb3f396 100644 --- a/build/bazel-generated-file-list.txt +++ b/build/bazel-generated-file-list.txt @@ -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 diff --git a/plugins/agent-workbench/chat/src/AgentChatVirtualFile.kt b/plugins/agent-workbench/chat/src/AgentChatVirtualFile.kt index cbec56da44d2..2aee3606706d 100644 --- a/plugins/agent-workbench/chat/src/AgentChatVirtualFile.kt +++ b/plugins/agent-workbench/chat/src/AgentChatVirtualFile.kt @@ -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 diff --git a/plugins/agent-workbench/chat/testSrc/AgentChatEditorServiceTest.kt b/plugins/agent-workbench/chat/testSrc/AgentChatEditorServiceTest.kt index 56774ab25950..8cb841f638a9 100644 --- a/plugins/agent-workbench/chat/testSrc/AgentChatEditorServiceTest.kt +++ b/plugins/agent-workbench/chat/testSrc/AgentChatEditorServiceTest.kt @@ -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().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().updateThread( - path = path, - provider = provider, - threadId = threadId, - title = title, - activity = null, activityReport = activityReport, ) return AgentChatOpenTabPresentationInvalidator.invalidate(changeSet) diff --git a/plugins/agent-workbench/chat/testSrc/AgentChatFileEditorLifecycleTest.kt b/plugins/agent-workbench/chat/testSrc/AgentChatFileEditorLifecycleTest.kt index c3e3d482a092..ad45566312ff 100644 --- a/plugins/agent-workbench/chat/testSrc/AgentChatFileEditorLifecycleTest.kt +++ b/plugins/agent-workbench/chat/testSrc/AgentChatFileEditorLifecycleTest.kt @@ -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"), ) ), diff --git a/plugins/agent-workbench/chat/testSrc/AgentChatFileEditorProviderTest.kt b/plugins/agent-workbench/chat/testSrc/AgentChatFileEditorProviderTest.kt index 908e9d4c06ab..62b6b4627235 100644 --- a/plugins/agent-workbench/chat/testSrc/AgentChatFileEditorProviderTest.kt +++ b/plugins/agent-workbench/chat/testSrc/AgentChatFileEditorProviderTest.kt @@ -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, ) } diff --git a/plugins/agent-workbench/chat/testSrc/AgentChatOpenTopLevelDispatchTest.kt b/plugins/agent-workbench/chat/testSrc/AgentChatOpenTopLevelDispatchTest.kt index 5c34a733580c..267583745e72 100644 --- a/plugins/agent-workbench/chat/testSrc/AgentChatOpenTopLevelDispatchTest.kt +++ b/plugins/agent-workbench/chat/testSrc/AgentChatOpenTopLevelDispatchTest.kt @@ -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( diff --git a/plugins/agent-workbench/lib-agent/core/api-dump.txt b/plugins/agent-workbench/lib-agent/core/api-dump.txt index 8be520f605b1..06c27eb3a5c9 100644 --- a/plugins/agent-workbench/lib-agent/core/api-dump.txt +++ b/plugins/agent-workbench/lib-agent/core/api-dump.txt @@ -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:(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:(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 diff --git a/plugins/agent-workbench/lib-agent/core/src/session/AgentSessionModels.kt b/plugins/agent-workbench/lib-agent/core/src/session/AgentSessionModels.kt index 9ee2e9d976f3..b8f284d44b88 100644 --- a/plugins/agent-workbench/lib-agent/core/src/session/AgentSessionModels.kt +++ b/plugins/agent-workbench/lib-agent/core/src/session/AgentSessionModels.kt @@ -50,36 +50,7 @@ data class AgentSessionThread( @JvmField val subAgents: List = 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 = 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. diff --git a/plugins/agent-workbench/lib-agent/providers/claude/sessions/src/ClaudeSessionSource.kt b/plugins/agent-workbench/lib-agent/providers/claude/sessions/src/ClaudeSessionSource.kt index b1f5883a1803..56222f7b8292 100644 --- a/plugins/agent-workbench/lib-agent/providers/claude/sessions/src/ClaudeSessionSource.kt +++ b/plugins/agent-workbench/lib-agent/providers/claude/sessions/src/ClaudeSessionSource.kt @@ -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, ) diff --git a/plugins/agent-workbench/lib-agent/providers/claude/sessions/testSrc/ClaudeSessionSourceTest.kt b/plugins/agent-workbench/lib-agent/providers/claude/sessions/testSrc/ClaudeSessionSourceTest.kt index 8482d687d9ca..b7a74d953614 100644 --- a/plugins/agent-workbench/lib-agent/providers/claude/sessions/testSrc/ClaudeSessionSourceTest.kt +++ b/plugins/agent-workbench/lib-agent/providers/claude/sessions/testSrc/ClaudeSessionSourceTest.kt @@ -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", diff --git a/plugins/agent-workbench/lib-agent/providers/codex/sessions/src/CodexSessionSource.kt b/plugins/agent-workbench/lib-agent/providers/codex/sessions/src/CodexSessionSource.kt index f8cffc315c6e..83e1bfcacc8b 100644 --- a/plugins/agent-workbench/lib-agent/providers/codex/sessions/src/CodexSessionSource.kt +++ b/plugins/agent-workbench/lib-agent/providers/codex/sessions/src/CodexSessionSource.kt @@ -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, ) } diff --git a/plugins/agent-workbench/lib-agent/providers/codex/sessions/testSrc/CodexSessionSourceRolloutIntegrationTest.kt b/plugins/agent-workbench/lib-agent/providers/codex/sessions/testSrc/CodexSessionSourceRolloutIntegrationTest.kt index ad06f87603a6..bc322930d0a3 100644 --- a/plugins/agent-workbench/lib-agent/providers/codex/sessions/testSrc/CodexSessionSourceRolloutIntegrationTest.kt +++ b/plugins/agent-workbench/lib-agent/providers/codex/sessions/testSrc/CodexSessionSourceRolloutIntegrationTest.kt @@ -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) } } diff --git a/plugins/agent-workbench/lib-agent/providers/codex/sessions/testSrc/CodexSessionSourceTest.kt b/plugins/agent-workbench/lib-agent/providers/codex/sessions/testSrc/CodexSessionSourceTest.kt index a99fcd6f36eb..b46a5a534739 100644 --- a/plugins/agent-workbench/lib-agent/providers/codex/sessions/testSrc/CodexSessionSourceTest.kt +++ b/plugins/agent-workbench/lib-agent/providers/codex/sessions/testSrc/CodexSessionSourceTest.kt @@ -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) } } } diff --git a/plugins/agent-workbench/lib-agent/providers/junie/sessions/src/JunieSessionSource.kt b/plugins/agent-workbench/lib-agent/providers/junie/sessions/src/JunieSessionSource.kt index 9cc061b94665..a281db1897e8 100644 --- a/plugins/agent-workbench/lib-agent/providers/junie/sessions/src/JunieSessionSource.kt +++ b/plugins/agent-workbench/lib-agent/providers/junie/sessions/src/JunieSessionSource.kt @@ -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, ) } diff --git a/plugins/agent-workbench/lib-agent/providers/junie/sessions/testSrc/JunieSessionSourceTest.kt b/plugins/agent-workbench/lib-agent/providers/junie/sessions/testSrc/JunieSessionSourceTest.kt index 5186b615905a..4c30e122cebb 100644 --- a/plugins/agent-workbench/lib-agent/providers/junie/sessions/testSrc/JunieSessionSourceTest.kt +++ b/plugins/agent-workbench/lib-agent/providers/junie/sessions/testSrc/JunieSessionSourceTest.kt @@ -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) } } diff --git a/plugins/agent-workbench/lib-agent/providers/opencode/sessions/src/OpenCodeSessionSource.kt b/plugins/agent-workbench/lib-agent/providers/opencode/sessions/src/OpenCodeSessionSource.kt index 945e6fb6fa52..c6c31c8f8fe2 100644 --- a/plugins/agent-workbench/lib-agent/providers/opencode/sessions/src/OpenCodeSessionSource.kt +++ b/plugins/agent-workbench/lib-agent/providers/opencode/sessions/src/OpenCodeSessionSource.kt @@ -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 - diff --git a/plugins/agent-workbench/lib-agent/providers/pi/sessions/intellij.platform.ai.agent.pi.sessions.tests.iml b/plugins/agent-workbench/lib-agent/providers/pi/sessions/intellij.platform.ai.agent.pi.sessions.tests.iml index 11f0067ddc1c..7330605fbd3a 100644 --- a/plugins/agent-workbench/lib-agent/providers/pi/sessions/intellij.platform.ai.agent.pi.sessions.tests.iml +++ b/plugins/agent-workbench/lib-agent/providers/pi/sessions/intellij.platform.ai.agent.pi.sessions.tests.iml @@ -25,6 +25,8 @@ + + diff --git a/plugins/agent-workbench/lib-agent/providers/pi/sessions/resources/intellij.platform.ai.agent.pi.sessions.xml b/plugins/agent-workbench/lib-agent/providers/pi/sessions/resources/intellij.platform.ai.agent.pi.sessions.xml index 05f54ba27503..09e1b0746e46 100644 --- a/plugins/agent-workbench/lib-agent/providers/pi/sessions/resources/intellij.platform.ai.agent.pi.sessions.xml +++ b/plugins/agent-workbench/lib-agent/providers/pi/sessions/resources/intellij.platform.ai.agent.pi.sessions.xml @@ -21,6 +21,10 @@ qualifiedName="com.intellij.agent.workbench.pi.jbCentralModelContributor" interface="com.intellij.platform.ai.agent.pi.sessions.PiJbCentralModelContributor" dynamic="true"/> + diff --git a/plugins/agent-workbench/lib-agent/providers/pi/sessions/resources/pi-extension/agent-workbench-extension.ts b/plugins/agent-workbench/lib-agent/providers/pi/sessions/resources/pi-extension/agent-workbench-extension.ts index 6b20cb7d62cd..a09e4c6e2590 100644 --- a/plugins/agent-workbench/lib-agent/providers/pi/sessions/resources/pi-extension/agent-workbench-extension.ts +++ b/plugins/agent-workbench/lib-agent/providers/pi/sessions/resources/pi-extension/agent-workbench-extension.ts @@ -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"; diff --git a/plugins/agent-workbench/lib-agent/providers/pi/sessions/resources/pi-extension/control.ts b/plugins/agent-workbench/lib-agent/providers/pi/sessions/resources/pi-extension/control.ts index 20f86faea915..e140df7c2f66 100644 --- a/plugins/agent-workbench/lib-agent/providers/pi/sessions/resources/pi-extension/control.ts +++ b/plugins/agent-workbench/lib-agent/providers/pi/sessions/resources/pi-extension/control.ts @@ -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: (operation: string, args?: Record) => Promise; + request: (type: string, payload?: Record) => Promise; 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; result?: unknown; }; @@ -50,24 +46,6 @@ type AgentWorkbenchControlThread = { activity: string; }; -type AgentWorkbenchTaskFolder = { - path: string; - id: string; - name: string; - status: string; - metadata?: Record; - createdAt?: number; - updatedAt?: number; -}; - -type AgentWorkbenchTaskFolderThread = { - path: string; - provider: string; - threadId: string; - folderId: string; - assignedAt?: number; -}; - type AgentWorkbenchControlContext = ExtensionContext & { navigateTree?: (entryId: string) => Promise | void; fork?: ( @@ -150,8 +128,8 @@ export function startControlBridge(ctx: ExtensionContext): AgentWorkbenchControl }; const sendRequest = ( - type: AgentWorkbenchControlRequestType, - payload: Partial = {}, + type: string, + payload: Record = {}, ): Promise => { 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 (operation: string, args: Record = {}): Promise => { - const response = requireOk(await sendRequest("taskFolderRequest", {operation, arguments: args})); + request: async (type: string, payload: Record = {}): Promise => { + 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; - operation: string; - arguments?: (params: Record) => Record; - confirm?: (params: Record, ctx: ExtensionContext) => Promise; - 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; - 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 { - 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 { - return typeof value === "object" && value !== null; -} - function resolveCapabilities(ctx: ExtensionContext): { navigateTree: boolean; fork: boolean } { const controlCtx = ctx as AgentWorkbenchControlContext; return { diff --git a/plugins/agent-workbench/lib-agent/providers/pi/sessions/resources/pi-extension/taskFolders.ts b/plugins/agent-workbench/lib-agent/providers/pi/sessions/resources/pi-extension/taskFolders.ts new file mode 100644 index 000000000000..36ccd86e05d3 --- /dev/null +++ b/plugins/agent-workbench/lib-agent/providers/pi/sessions/resources/pi-extension/taskFolders.ts @@ -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; + 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; + operation: string; + arguments?: (params: Record) => Record; + confirm?: (params: Record, ctx: ExtensionContext) => Promise; + 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; + 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 { + 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 { + return typeof value === "object" && value !== null; +} diff --git a/plugins/agent-workbench/lib-agent/providers/pi/sessions/src/PiAgentSessionProviderId.kt b/plugins/agent-workbench/lib-agent/providers/pi/sessions/src/PiAgentSessionProviderId.kt index 68f8cb4e7459..87ef65dc3134 100644 --- a/plugins/agent-workbench/lib-agent/providers/pi/sessions/src/PiAgentSessionProviderId.kt +++ b/plugins/agent-workbench/lib-agent/providers/pi/sessions/src/PiAgentSessionProviderId.kt @@ -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") diff --git a/plugins/agent-workbench/lib-agent/providers/pi/sessions/src/PiControlRequestHandler.kt b/plugins/agent-workbench/lib-agent/providers/pi/sessions/src/PiControlRequestHandler.kt new file mode 100644 index 000000000000..7ea6725cbc98 --- /dev/null +++ b/plugins/agent-workbench/lib-agent/providers/pi/sessions/src/PiControlRequestHandler.kt @@ -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 = + ExtensionPointName("com.intellij.agent.workbench.pi.controlRequestHandler") + } +} diff --git a/plugins/agent-workbench/lib-agent/providers/pi/sessions/src/PiExtensionControlBridge.kt b/plugins/agent-workbench/lib-agent/providers/pi/sessions/src/PiExtensionControlBridge.kt index efbdc9cea5a6..c54820790514 100644 --- a/plugins/agent-workbench/lib-agent/providers/pi/sessions/src/PiExtensionControlBridge.kt +++ b/plugins/agent-workbench/lib-agent/providers/pi/sessions/src/PiExtensionControlBridge.kt @@ -30,7 +30,6 @@ private val CONTROL_LOG = logger() private val PI_CONTROL_CONNECTION_KEY: Key = Key.create("agent.workbench.pi.control.connection") internal object PiExtensionControlBridge { - private val taskFolderControlHandler = PiTaskFolderControlHandler() private val connectionsBySessionId = ConcurrentHashMap() private val controlUpdates = MutableSharedFlow( 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? { diff --git a/plugins/agent-workbench/lib-agent/providers/pi/sessions/src/PiExtensionControlProtocol.kt b/plugins/agent-workbench/lib-agent/providers/pi/sessions/src/PiExtensionControlProtocol.kt index dd4ee1796923..094ae848a170 100644 --- a/plugins/agent-workbench/lib-agent/providers/pi/sessions/src/PiExtensionControlProtocol.kt +++ b/plugins/agent-workbench/lib-agent/providers/pi/sessions/src/PiExtensionControlProtocol.kt @@ -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 = 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, diff --git a/plugins/agent-workbench/lib-agent/providers/pi/sessions/src/PiSessionSource.kt b/plugins/agent-workbench/lib-agent/providers/pi/sessions/src/PiSessionSource.kt index 323479b2250c..246ec82ffb90 100644 --- a/plugins/agent-workbench/lib-agent/providers/pi/sessions/src/PiSessionSource.kt +++ b/plugins/agent-workbench/lib-agent/providers/pi/sessions/src/PiSessionSource.kt @@ -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 = listOf( "omlx.ts", "status.ts", "terminalInput.ts", + "taskFolders.ts", "theme.ts", ) private const val PI_EXTENSION_FILE_NAME_HASH_PREFIX_LENGTH: Int = 16 diff --git a/plugins/agent-workbench/lib-agent/providers/pi/sessions/testSrc/PiAgentSessionProviderDescriptorTest.kt b/plugins/agent-workbench/lib-agent/providers/pi/sessions/testSrc/PiAgentSessionProviderDescriptorTest.kt index 173853846564..4cbe6d0a36d3 100644 --- a/plugins/agent-workbench/lib-agent/providers/pi/sessions/testSrc/PiAgentSessionProviderDescriptorTest.kt +++ b/plugins/agent-workbench/lib-agent/providers/pi/sessions/testSrc/PiAgentSessionProviderDescriptorTest.kt @@ -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 = emptyList() diff --git a/plugins/agent-workbench/lib-agent/providers/pi/sessions/testSrc/PiExtensionControlWebSocketHandlerTest.kt b/plugins/agent-workbench/lib-agent/providers/pi/sessions/testSrc/PiExtensionControlWebSocketHandlerTest.kt index fd93308eaf43..51172e5a2146 100644 --- a/plugins/agent-workbench/lib-agent/providers/pi/sessions/testSrc/PiExtensionControlWebSocketHandlerTest.kt +++ b/plugins/agent-workbench/lib-agent/providers/pi/sessions/testSrc/PiExtensionControlWebSocketHandlerTest.kt @@ -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) } diff --git a/plugins/agent-workbench/lib-agent/providers/pi/sessions/testSrc/PiSessionSourceTest.kt b/plugins/agent-workbench/lib-agent/providers/pi/sessions/testSrc/PiSessionSourceTest.kt index bc687eda6927..779d80157fb9 100644 --- a/plugins/agent-workbench/lib-agent/providers/pi/sessions/testSrc/PiSessionSourceTest.kt +++ b/plugins/agent-workbench/lib-agent/providers/pi/sessions/testSrc/PiSessionSourceTest.kt @@ -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) } } diff --git a/plugins/agent-workbench/lib-agent/providers/pi/sessions/testSrc/PiThemeSupportTest.kt b/plugins/agent-workbench/lib-agent/providers/pi/sessions/testSrc/PiThemeSupportTest.kt index 4b914ffbe018..f4613f55a85f 100644 --- a/plugins/agent-workbench/lib-agent/providers/pi/sessions/testSrc/PiThemeSupportTest.kt +++ b/plugins/agent-workbench/lib-agent/providers/pi/sessions/testSrc/PiThemeSupportTest.kt @@ -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", ) } diff --git a/plugins/agent-workbench/lib-agent/providers/terminal/sessions/src/TerminalSessionStateService.kt b/plugins/agent-workbench/lib-agent/providers/terminal/sessions/src/TerminalSessionStateService.kt index d2058139b8a6..0e210296483d 100644 --- a/plugins/agent-workbench/lib-agent/providers/terminal/sessions/src/TerminalSessionStateService.kt +++ b/plugins/agent-workbench/lib-agent/providers/terminal/sessions/src/TerminalSessionStateService.kt @@ -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, ) } diff --git a/plugins/agent-workbench/lib-agent/sessions-core/api-dump.txt b/plugins/agent-workbench/lib-agent/sessions-core/api-dump.txt index 573ce5c8893f..f809135eceb2 100644 --- a/plugins/agent-workbench/lib-agent/sessions-core/api-dump.txt +++ b/plugins/agent-workbench/lib-agent/sessions-core/api-dump.txt @@ -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 -- (java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.AgentThreadActivity):V - (java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.AgentThreadActivityReport,java.lang.Long):V - b:(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 -- (java.lang.String,com.intellij.platform.ai.agent.core.AgentThreadActivity):V - (java.lang.String,com.intellij.platform.ai.agent.core.AgentThreadActivityReport,java.lang.Long):V - b:(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 -- (java.lang.String,java.lang.String,java.lang.String,Z,java.util.Map):V -- b:(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:(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 diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/AgentSessionThreadPresentationModel.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/AgentSessionThreadPresentationModel.kt index 3e6f088b3df1..b8bbdbb72f91 100644 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/AgentSessionThreadPresentationModel.kt +++ b/plugins/agent-workbench/lib-agent/sessions-core/src/AgentSessionThreadPresentationModel.kt @@ -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, @@ -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) diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/SessionActionTarget.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/SessionActionTarget.kt index 66ee72bf3ec6..3468c34dfde7 100644 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/SessionActionTarget.kt +++ b/plugins/agent-workbench/lib-agent/sessions-core/src/SessionActionTarget.kt @@ -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 = emptyMap(), - ) : SessionActionTarget - sealed interface Conversation : SessionActionTarget { val path: String val provider: AgentSessionProvider diff --git a/plugins/agent-workbench/pi/awb/BUILD.bazel b/plugins/agent-workbench/pi/awb/BUILD.bazel index e0c8ece60f99..e0c675145910 100644 --- a/plugins/agent-workbench/pi/awb/BUILD.bazel +++ b/plugins/agent-workbench/pi/awb/BUILD.bazel @@ -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 diff --git a/plugins/agent-workbench/pi/awb/intellij.agent.workbench.pi.awb.iml b/plugins/agent-workbench/pi/awb/intellij.agent.workbench.pi.awb.iml index f5f9eb22d1fd..814980452167 100644 --- a/plugins/agent-workbench/pi/awb/intellij.agent.workbench.pi.awb.iml +++ b/plugins/agent-workbench/pi/awb/intellij.agent.workbench.pi.awb.iml @@ -14,7 +14,9 @@ - + + + \ No newline at end of file diff --git a/plugins/agent-workbench/pi/awb/resources/intellij.agent.workbench.pi.awb.xml b/plugins/agent-workbench/pi/awb/resources/intellij.agent.workbench.pi.awb.xml index 4fd84ce58886..0592ad262f16 100644 --- a/plugins/agent-workbench/pi/awb/resources/intellij.agent.workbench.pi.awb.xml +++ b/plugins/agent-workbench/pi/awb/resources/intellij.agent.workbench.pi.awb.xml @@ -3,12 +3,14 @@ - + + + diff --git a/plugins/agent-workbench/pi/awb/resources/messages/PiSessionsBundle.properties b/plugins/agent-workbench/pi/awb/resources/messages/PiSessionsBundle.properties new file mode 100644 index 000000000000..22785a3bbab8 --- /dev/null +++ b/plugins/agent-workbench/pi/awb/resources/messages/PiSessionsBundle.properties @@ -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. diff --git a/plugins/agent-workbench/pi/awb/src/PiProviderSettingsContributor.kt b/plugins/agent-workbench/pi/awb/src/PiProviderSettingsContributor.kt index 2a4db20877e4..e70cd7676b42 100644 --- a/plugins/agent-workbench/pi/awb/src/PiProviderSettingsContributor.kt +++ b/plugins/agent-workbench/pi/awb/src/PiProviderSettingsContributor.kt @@ -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 { 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, ) diff --git a/plugins/agent-workbench/pi/awb/src/PiSessionsBundle.kt b/plugins/agent-workbench/pi/awb/src/PiSessionsBundle.kt new file mode 100644 index 000000000000..fa3ca0d0ccdf --- /dev/null +++ b/plugins/agent-workbench/pi/awb/src/PiSessionsBundle.kt @@ -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) + } +} + diff --git a/plugins/agent-workbench/lib-agent/providers/pi/sessions/src/PiTaskFolderControlHandler.kt b/plugins/agent-workbench/pi/awb/src/PiTaskFolderControlHandler.kt similarity index 80% rename from plugins/agent-workbench/lib-agent/providers/pi/sessions/src/PiTaskFolderControlHandler.kt rename to plugins/agent-workbench/pi/awb/src/PiTaskFolderControlHandler.kt index 4652e6fd0b83..defa3498eb37 100644 --- a/plugins/agent-workbench/lib-agent/providers/pi/sessions/src/PiTaskFolderControlHandler.kt +++ b/plugins/agent-workbench/pi/awb/src/PiTaskFolderControlHandler.kt @@ -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() }, - private val archiveServiceProvider: () -> AgentSessionArchiveService = { service() }, -) { - fun handle( +@ApiStatus.Internal +class PiTaskFolderControlHandler : PiControlRequestHandler { + private val taskFolderServiceProvider: () -> AgentTaskFolderService + + constructor() : this( + taskFolderServiceProvider = { service() }, + ) + + 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): 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" diff --git a/plugins/agent-workbench/plugin-content.yaml b/plugins/agent-workbench/plugin-content.yaml index c602740f7ba3..407ddaaa432f 100644 --- a/plugins/agent-workbench/plugin-content.yaml +++ b/plugins/agent-workbench/plugin-content.yaml @@ -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 diff --git a/plugins/agent-workbench/plugin/BUILD.bazel b/plugins/agent-workbench/plugin/BUILD.bazel index 3455d9bd2437..c704e3133d8b 100644 --- a/plugins/agent-workbench/plugin/BUILD.bazel +++ b/plugins/agent-workbench/plugin/BUILD.bazel @@ -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 diff --git a/plugins/agent-workbench/plugin/intellij.agent.workbench.plugin.tests.iml b/plugins/agent-workbench/plugin/intellij.agent.workbench.plugin.tests.iml index c8fad60bc6bd..ea2215c7c777 100644 --- a/plugins/agent-workbench/plugin/intellij.agent.workbench.plugin.tests.iml +++ b/plugins/agent-workbench/plugin/intellij.agent.workbench.plugin.tests.iml @@ -93,6 +93,7 @@ + \ No newline at end of file diff --git a/plugins/agent-workbench/plugin/plugin-content.yaml b/plugins/agent-workbench/plugin/plugin-content.yaml index c0a4b285f29f..70f22a952a40 100644 --- a/plugins/agent-workbench/plugin/plugin-content.yaml +++ b/plugins/agent-workbench/plugin/plugin-content.yaml @@ -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 diff --git a/plugins/agent-workbench/plugin/resources/META-INF/plugin.xml b/plugins/agent-workbench/plugin/resources/META-INF/plugin.xml index a24c21680be5..3bc7afc8c81e 100644 --- a/plugins/agent-workbench/plugin/resources/META-INF/plugin.xml +++ b/plugins/agent-workbench/plugin/resources/META-INF/plugin.xml @@ -49,6 +49,7 @@ + diff --git a/plugins/agent-workbench/prompt/ui/src/AgentPromptExistingTaskController.kt b/plugins/agent-workbench/prompt/ui/src/AgentPromptExistingTaskController.kt index c60affb23857..b9967be23377 100644 --- a/plugins/agent-workbench/prompt/ui/src/AgentPromptExistingTaskController.kt +++ b/plugins/agent-workbench/prompt/ui/src/AgentPromptExistingTaskController.kt @@ -218,7 +218,7 @@ internal fun formatExistingTaskEntries( nowLabel = nowLabel, unknownLabel = unknownLabel, ), - activity = thread.activity, + activity = thread.activityReport.rowActivity, ) } .toList() diff --git a/plugins/agent-workbench/prompt/ui/testSrc/AgentPromptExistingTaskControllerTest.kt b/plugins/agent-workbench/prompt/ui/testSrc/AgentPromptExistingTaskControllerTest.kt index e4bfccd613fb..e553851d1ad9 100644 --- a/plugins/agent-workbench/prompt/ui/testSrc/AgentPromptExistingTaskControllerTest.kt +++ b/plugins/agent-workbench/prompt/ui/testSrc/AgentPromptExistingTaskControllerTest.kt @@ -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"), ) } diff --git a/plugins/agent-workbench/prompt/ui/testSrc/AgentPromptPaletteSessionControllerTest.kt b/plugins/agent-workbench/prompt/ui/testSrc/AgentPromptPaletteSessionControllerTest.kt index b0e2532b29b9..9bc84fe89b21 100644 --- a/plugins/agent-workbench/prompt/ui/testSrc/AgentPromptPaletteSessionControllerTest.kt +++ b/plugins/agent-workbench/prompt/ui/testSrc/AgentPromptPaletteSessionControllerTest.kt @@ -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, ) } diff --git a/plugins/agent-workbench/prompt/ui/testSrc/AgentPromptPaletteSubmitControllerTest.kt b/plugins/agent-workbench/prompt/ui/testSrc/AgentPromptPaletteSubmitControllerTest.kt index f166f643868c..473ea1a47149 100644 --- a/plugins/agent-workbench/prompt/ui/testSrc/AgentPromptPaletteSubmitControllerTest.kt +++ b/plugins/agent-workbench/prompt/ui/testSrc/AgentPromptPaletteSubmitControllerTest.kt @@ -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(), ) ), diff --git a/plugins/agent-workbench/sessions-task-folders/BUILD.bazel b/plugins/agent-workbench/sessions-task-folders/BUILD.bazel new file mode 100644 index 000000000000..e8bb8015f5df --- /dev/null +++ b/plugins/agent-workbench/sessions-task-folders/BUILD.bazel @@ -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 diff --git a/plugins/agent-workbench/sessions-task-folders/intellij.agent.workbench.sessions.task.folders.iml b/plugins/agent-workbench/sessions-task-folders/intellij.agent.workbench.sessions.task.folders.iml new file mode 100644 index 000000000000..eedb99dce973 --- /dev/null +++ b/plugins/agent-workbench/sessions-task-folders/intellij.agent.workbench.sessions.task.folders.iml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + $KOTLIN_BUNDLED$/lib/kotlinx-serialization-compiler-plugin.jar + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/agent-workbench/sessions-task-folders/intellij.agent.workbench.sessions.task.folders.tests.iml b/plugins/agent-workbench/sessions-task-folders/intellij.agent.workbench.sessions.task.folders.tests.iml new file mode 100644 index 000000000000..a0eb3a65c1a5 --- /dev/null +++ b/plugins/agent-workbench/sessions-task-folders/intellij.agent.workbench.sessions.task.folders.tests.iml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + $KOTLIN_BUNDLED$/lib/kotlinx-serialization-compiler-plugin.jar + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/agent-workbench/sessions-task-folders/resources/intellij.agent.workbench.sessions.task.folders.xml b/plugins/agent-workbench/sessions-task-folders/resources/intellij.agent.workbench.sessions.task.folders.xml new file mode 100644 index 000000000000..5c833a56b5f1 --- /dev/null +++ b/plugins/agent-workbench/sessions-task-folders/resources/intellij.agent.workbench.sessions.task.folders.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/folders/AgentTaskFolders.kt b/plugins/agent-workbench/sessions-task-folders/src/AgentTaskFolders.kt similarity index 99% rename from plugins/agent-workbench/lib-agent/sessions-core/src/folders/AgentTaskFolders.kt rename to plugins/agent-workbench/sessions-task-folders/src/AgentTaskFolders.kt index c1fac6342b8e..9ecdbc5d7ed0 100644 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/folders/AgentTaskFolders.kt +++ b/plugins/agent-workbench/sessions-task-folders/src/AgentTaskFolders.kt @@ -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 diff --git a/plugins/agent-workbench/sessions/testSrc/AgentTaskFolderServiceTest.kt b/plugins/agent-workbench/sessions-task-folders/testSrc/AgentTaskFolderServiceTest.kt similarity index 96% rename from plugins/agent-workbench/sessions/testSrc/AgentTaskFolderServiceTest.kt rename to plugins/agent-workbench/sessions-task-folders/testSrc/AgentTaskFolderServiceTest.kt index 94e69405bad4..b07407cee695 100644 --- a/plugins/agent-workbench/sessions/testSrc/AgentTaskFolderServiceTest.kt +++ b/plugins/agent-workbench/sessions-task-folders/testSrc/AgentTaskFolderServiceTest.kt @@ -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 diff --git a/plugins/agent-workbench/sessions-toolwindow/BUILD.bazel b/plugins/agent-workbench/sessions-toolwindow/BUILD.bazel index 0205aa770551..15eadc2af54f 100644 --- a/plugins/agent-workbench/sessions-toolwindow/BUILD.bazel +++ b/plugins/agent-workbench/sessions-toolwindow/BUILD.bazel @@ -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", diff --git a/plugins/agent-workbench/sessions-toolwindow/intellij.agent.workbench.sessions.toolwindow.iml b/plugins/agent-workbench/sessions-toolwindow/intellij.agent.workbench.sessions.toolwindow.iml index 820ae4e91d72..1ce9528fbe88 100644 --- a/plugins/agent-workbench/sessions-toolwindow/intellij.agent.workbench.sessions.toolwindow.iml +++ b/plugins/agent-workbench/sessions-toolwindow/intellij.agent.workbench.sessions.toolwindow.iml @@ -59,6 +59,7 @@ + diff --git a/plugins/agent-workbench/sessions-toolwindow/intellij.agent.workbench.sessions.toolwindow.tests.iml b/plugins/agent-workbench/sessions-toolwindow/intellij.agent.workbench.sessions.toolwindow.tests.iml index 14bdb2bfe48b..8de68b466711 100644 --- a/plugins/agent-workbench/sessions-toolwindow/intellij.agent.workbench.sessions.toolwindow.tests.iml +++ b/plugins/agent-workbench/sessions-toolwindow/intellij.agent.workbench.sessions.toolwindow.tests.iml @@ -57,6 +57,7 @@ + diff --git a/plugins/agent-workbench/sessions-toolwindow/resources/intellij.agent.workbench.sessions.toolwindow.xml b/plugins/agent-workbench/sessions-toolwindow/resources/intellij.agent.workbench.sessions.toolwindow.xml index 58fd5224c502..6f76891c710c 100644 --- a/plugins/agent-workbench/sessions-toolwindow/resources/intellij.agent.workbench.sessions.toolwindow.xml +++ b/plugins/agent-workbench/sessions-toolwindow/resources/intellij.agent.workbench.sessions.toolwindow.xml @@ -8,6 +8,7 @@ + diff --git a/plugins/agent-workbench/sessions-toolwindow/src/actions/AgentSessionsTreePopupActions.kt b/plugins/agent-workbench/sessions-toolwindow/src/actions/AgentSessionsTreePopupActions.kt index 60e65435c479..af562432d1f9 100644 --- a/plugins/agent-workbench/sessions-toolwindow/src/actions/AgentSessionsTreePopupActions.kt +++ b/plugins/agent-workbench/sessions-toolwindow/src/actions/AgentSessionsTreePopupActions.kt @@ -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, @JvmField val unarchiveTargets: List = emptyList(), @JvmField val selectedThreadTargets: List = 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 = 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 -> diff --git a/plugins/agent-workbench/sessions-toolwindow/src/actions/AgentSessionsTreePopupTaskFolderActions.kt b/plugins/agent-workbench/sessions-toolwindow/src/actions/AgentSessionsTreePopupTaskFolderActions.kt index 2b99829cb13a..41b919036284 100644 --- a/plugins/agent-workbench/sessions-toolwindow/src/actions/AgentSessionsTreePopupTaskFolderActions.kt +++ b/plugins/agent-workbench/sessions-toolwindow/src/actions/AgentSessionsTreePopupTaskFolderActions.kt @@ -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().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().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().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, 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, 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)) diff --git a/plugins/agent-workbench/sessions-toolwindow/src/actions/AgentSessionsTreePopupTaskFolderAgentActions.kt b/plugins/agent-workbench/sessions-toolwindow/src/actions/AgentSessionsTreePopupTaskFolderAgentActions.kt index a3c8d921b5f4..10f5a3b6060a 100644 --- a/plugins/agent-workbench/sessions-toolwindow/src/actions/AgentSessionsTreePopupTaskFolderAgentActions.kt +++ b/plugins/agent-workbench/sessions-toolwindow/src/actions/AgentSessionsTreePopupTaskFolderAgentActions.kt @@ -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 diff --git a/plugins/agent-workbench/sessions-toolwindow/src/actions/SessionTreeActionTargets.kt b/plugins/agent-workbench/sessions-toolwindow/src/actions/SessionTreeActionTargets.kt index ed3f7d1b65c1..c6faa135ed78 100644 --- a/plugins/agent-workbench/sessions-toolwindow/src/actions/SessionTreeActionTargets.kt +++ b/plugins/agent-workbench/sessions-toolwindow/src/actions/SessionTreeActionTargets.kt @@ -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 = 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, + ) +} diff --git a/plugins/agent-workbench/sessions-toolwindow/src/tree/PendingAgentChatTreeOverlay.kt b/plugins/agent-workbench/sessions-toolwindow/src/tree/PendingAgentChatTreeOverlay.kt index d4f02e64a52a..cb80fbf31a27 100644 --- a/plugins/agent-workbench/sessions-toolwindow/src/tree/PendingAgentChatTreeOverlay.kt +++ b/plugins/agent-workbench/sessions-toolwindow/src/tree/PendingAgentChatTreeOverlay.kt @@ -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, ) } diff --git a/plugins/agent-workbench/sessions-toolwindow/src/tree/SessionTree.kt b/plugins/agent-workbench/sessions-toolwindow/src/tree/SessionTree.kt index 3f3fb2fc83af..0fb519088acf 100644 --- a/plugins/agent-workbench/sessions-toolwindow/src/tree/SessionTree.kt +++ b/plugins/agent-workbench/sessions-toolwindow/src/tree/SessionTree.kt @@ -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( diff --git a/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsActivityService.kt b/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsActivityService.kt index ebdc39ff741d..e35f26c42a60 100644 --- a/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsActivityService.kt +++ b/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsActivityService.kt @@ -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}" } } diff --git a/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsTitleActions.kt b/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsTitleActions.kt index 4c57e61eb853..3171c912c03c 100644 --- a/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsTitleActions.kt +++ b/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsTitleActions.kt @@ -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) } diff --git a/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsToolWindow.kt b/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsToolWindow.kt index 9c875d7b318f..46437523d910 100644 --- a/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsToolWindow.kt +++ b/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsToolWindow.kt @@ -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 diff --git a/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsTreeCellRenderer.kt b/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsTreeCellRenderer.kt index 47da6ae68fac..614d1fccd0f2 100644 --- a/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsTreeCellRenderer.kt +++ b/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsTreeCellRenderer.kt @@ -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 { diff --git a/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsTreeInteractionController.kt b/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsTreeInteractionController.kt index aabe338e55a0..23450a1dd1f2 100644 --- a/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsTreeInteractionController.kt +++ b/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsTreeInteractionController.kt @@ -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 diff --git a/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsTreeStateController.kt b/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsTreeStateController.kt index 8b2d3d4b13c1..eb9c09dfc176 100644 --- a/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsTreeStateController.kt +++ b/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsTreeStateController.kt @@ -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 diff --git a/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsTreeTaskFolderDnDSupport.kt b/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsTreeTaskFolderDnDSupport.kt index 9e9e562faa33..557aa991712b 100644 --- a/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsTreeTaskFolderDnDSupport.kt +++ b/plugins/agent-workbench/sessions-toolwindow/src/ui/AgentSessionsTreeTaskFolderDnDSupport.kt @@ -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 diff --git a/plugins/agent-workbench/sessions-toolwindow/src/ui/SessionTreeThreadPresentation.kt b/plugins/agent-workbench/sessions-toolwindow/src/ui/SessionTreeThreadPresentation.kt index 2e4fe372360b..cc69f13ebad3 100644 --- a/plugins/agent-workbench/sessions-toolwindow/src/ui/SessionTreeThreadPresentation.kt +++ b/plugins/agent-workbench/sessions-toolwindow/src/ui/SessionTreeThreadPresentation.kt @@ -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 diff --git a/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsActivitySummaryTest.kt b/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsActivitySummaryTest.kt index c258c7f6f457..ac27d14b5955 100644 --- a/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsActivitySummaryTest.kt +++ b/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsActivitySummaryTest.kt @@ -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, ) } } diff --git a/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsCodexActivityRenderingIntegrationTest.kt b/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsCodexActivityRenderingIntegrationTest.kt index 106a2c5e1457..4a461d14747f 100644 --- a/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsCodexActivityRenderingIntegrationTest.kt +++ b/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsCodexActivityRenderingIntegrationTest.kt @@ -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), ) ) } diff --git a/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsMainToolbarActivityGroupTest.kt b/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsMainToolbarActivityGroupTest.kt index 6000778e155a..0a836ef38b18 100644 --- a/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsMainToolbarActivityGroupTest.kt +++ b/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsMainToolbarActivityGroupTest.kt @@ -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, ) } } diff --git a/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsSwingTreeCellRendererTest.kt b/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsSwingTreeCellRendererTest.kt index 8fd0001c7d8f..2053683395ba 100644 --- a/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsSwingTreeCellRendererTest.kt +++ b/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsSwingTreeCellRendererTest.kt @@ -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) diff --git a/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsTreeModelDiffTest.kt b/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsTreeModelDiffTest.kt index adbf6744d75d..5a7b7a11ee15 100644 --- a/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsTreeModelDiffTest.kt +++ b/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsTreeModelDiffTest.kt @@ -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"), ) ), diff --git a/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsTreePopupActionsTest.kt b/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsTreePopupActionsTest.kt index d3cd5071d9d4..2208b5a1486d 100644 --- a/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsTreePopupActionsTest.kt +++ b/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsTreePopupActionsTest.kt @@ -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? = 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", diff --git a/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsTreeSnapshotTest.kt b/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsTreeSnapshotTest.kt index dc7ebe83ff0e..8661c66f3900 100644 --- a/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsTreeSnapshotTest.kt +++ b/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsTreeSnapshotTest.kt @@ -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")) } diff --git a/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsTreeStateControllerTest.kt b/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsTreeStateControllerTest.kt index e3e5d23c9277..68ddee6b7f52 100644 --- a/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsTreeStateControllerTest.kt +++ b/plugins/agent-workbench/sessions-toolwindow/testSrc/AgentSessionsTreeStateControllerTest.kt @@ -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), ) } diff --git a/plugins/agent-workbench/sessions/resources/messages/AgentSessionsBundle.properties b/plugins/agent-workbench/sessions/resources/messages/AgentSessionsBundle.properties index 5d3cf866063a..3eac1e1ebcd1 100644 --- a/plugins/agent-workbench/sessions/resources/messages/AgentSessionsBundle.properties +++ b/plugins/agent-workbench/sessions/resources/messages/AgentSessionsBundle.properties @@ -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? diff --git a/plugins/agent-workbench/sessions/src/service/AgentSessionContentRepository.kt b/plugins/agent-workbench/sessions/src/service/AgentSessionContentRepository.kt index 7ddd615c8327..add915e9253f 100644 --- a/plugins/agent-workbench/sessions/src/service/AgentSessionContentRepository.kt +++ b/plugins/agent-workbench/sessions/src/service/AgentSessionContentRepository.kt @@ -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, ), ) } diff --git a/plugins/agent-workbench/sessions/src/service/AgentSessionLaunchService.kt b/plugins/agent-workbench/sessions/src/service/AgentSessionLaunchService.kt index 1ae6e60d2f7d..ba50033080a1 100644 --- a/plugins/agent-workbench/sessions/src/service/AgentSessionLaunchService.kt +++ b/plugins/agent-workbench/sessions/src/service/AgentSessionLaunchService.kt @@ -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, diff --git a/plugins/agent-workbench/sessions/src/service/AgentSessionReadService.kt b/plugins/agent-workbench/sessions/src/service/AgentSessionReadService.kt index d858a61a5d66..a0cb9fa2b460 100644 --- a/plugins/agent-workbench/sessions/src/service/AgentSessionReadService.kt +++ b/plugins/agent-workbench/sessions/src/service/AgentSessionReadService.kt @@ -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, ) } diff --git a/plugins/agent-workbench/sessions/src/service/AgentSessionRefreshCoordinator.kt b/plugins/agent-workbench/sessions/src/service/AgentSessionRefreshCoordinator.kt index 7d5a32ae73a8..188d5f4f902f 100644 --- a/plugins/agent-workbench/sessions/src/service/AgentSessionRefreshCoordinator.kt +++ b/plugins/agent-workbench/sessions/src/service/AgentSessionRefreshCoordinator.kt @@ -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( diff --git a/plugins/agent-workbench/sessions/src/service/AgentSessionRenameService.kt b/plugins/agent-workbench/sessions/src/service/AgentSessionRenameService.kt index 85a11aaf3380..c54a20a2bea9 100644 --- a/plugins/agent-workbench/sessions/src/service/AgentSessionRenameService.kt +++ b/plugins/agent-workbench/sessions/src/service/AgentSessionRenameService.kt @@ -179,7 +179,6 @@ class AgentSessionRenameService internal constructor( provider = target.provider, threadId = target.threadId, title = normalizedRequestedName, - activity = null, activityReport = activityReport, ) } diff --git a/plugins/agent-workbench/sessions/src/service/AgentSessionThreadRebindSupport.kt b/plugins/agent-workbench/sessions/src/service/AgentSessionThreadRebindSupport.kt index f87269f05277..cd59d15cf125 100644 --- a/plugins/agent-workbench/sessions/src/service/AgentSessionThreadRebindSupport.kt +++ b/plugins/agent-workbench/sessions/src/service/AgentSessionThreadRebindSupport.kt @@ -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, ) ) diff --git a/plugins/agent-workbench/sessions/src/service/AgentSessionUnreadActivitySupport.kt b/plugins/agent-workbench/sessions/src/service/AgentSessionUnreadActivitySupport.kt index ccf3d787967c..f7384dcd9fd7 100644 --- a/plugins/agent-workbench/sessions/src/service/AgentSessionUnreadActivitySupport.kt +++ b/plugins/agent-workbench/sessions/src/service/AgentSessionUnreadActivitySupport.kt @@ -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 } diff --git a/plugins/agent-workbench/sessions/src/service/AgentSessionVisibleCostHydrationSupport.kt b/plugins/agent-workbench/sessions/src/service/AgentSessionVisibleCostHydrationSupport.kt index 835a50a47887..6afc6c84e4c1 100644 --- a/plugins/agent-workbench/sessions/src/service/AgentSessionVisibleCostHydrationSupport.kt +++ b/plugins/agent-workbench/sessions/src/service/AgentSessionVisibleCostHydrationSupport.kt @@ -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) diff --git a/plugins/agent-workbench/sessions/src/sleep/AgentSessionSleepPreventionService.kt b/plugins/agent-workbench/sessions/src/sleep/AgentSessionSleepPreventionService.kt index a5e1a08c42ac..08afa8edf3d0 100644 --- a/plugins/agent-workbench/sessions/src/sleep/AgentSessionSleepPreventionService.kt +++ b/plugins/agent-workbench/sessions/src/sleep/AgentSessionSleepPreventionService.kt @@ -255,7 +255,7 @@ private fun createPowerSaveModeFlow(serviceScope: CoroutineScope): StateFlow - 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 } } } } diff --git a/plugins/agent-workbench/sessions/src/state/AgentSessionWarmStateService.kt b/plugins/agent-workbench/sessions/src/state/AgentSessionWarmStateService.kt index 9b30b85f6706..b027e1cc3c7d 100644 --- a/plugins/agent-workbench/sessions/src/state/AgentSessionWarmStateService.kt +++ b/plugins/agent-workbench/sessions/src/state/AgentSessionWarmStateService.kt @@ -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(), ) }, diff --git a/plugins/agent-workbench/sessions/testSrc/AgentSessionIntegrationTestSupport.kt b/plugins/agent-workbench/sessions/testSrc/AgentSessionIntegrationTestSupport.kt index 97419fcb99bb..2e97995b724f 100644 --- a/plugins/agent-workbench/sessions/testSrc/AgentSessionIntegrationTestSupport.kt +++ b/plugins/agent-workbench/sessions/testSrc/AgentSessionIntegrationTestSupport.kt @@ -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 = 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, ) diff --git a/plugins/agent-workbench/sessions/testSrc/AgentSessionRefreshCoordinatorTest.kt b/plugins/agent-workbench/sessions/testSrc/AgentSessionRefreshCoordinatorTest.kt index d0133e187182..e5cac275bdfc 100644 --- a/plugins/agent-workbench/sessions/testSrc/AgentSessionRefreshCoordinatorTest.kt +++ b/plugins/agent-workbench/sessions/testSrc/AgentSessionRefreshCoordinatorTest.kt @@ -10,6 +10,7 @@ import com.intellij.agent.workbench.chat.AgentChatPendingTabRebindStatus import com.intellij.agent.workbench.chat.AgentChatPendingTabSnapshot import com.intellij.agent.workbench.chat.AgentChatTabRebindTarget 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.platform.ai.agent.core.session.AgentSubAgent @@ -1153,7 +1154,7 @@ class AgentSessionRefreshCoordinatorTest { val expectedKey = presentationKey(projectB, AgentSessionProvider.from("codex"), "codex-open") waitForCondition { presentationModel.snapshot()[expectedKey]?.title == "Open concrete tab thread" && - presentationModel.snapshot()[expectedKey]?.activity == AgentThreadActivity.UNREAD + presentationModel.snapshot()[expectedKey]?.activityReport?.rowActivity == AgentThreadActivity.UNREAD } assertThat(closedRefreshInvocations[PROJECT_PATH]?.get() ?: 0).isEqualTo(0) @@ -1676,7 +1677,7 @@ class AgentSessionRefreshCoordinatorTest { val expectedKey = presentationKey(PROJECT_PATH, AgentSessionProvider.from("codex"), "codex-1") waitForCondition { presentationModel.snapshot()[expectedKey]?.title == "Renamed from source update" && - presentationModel.snapshot()[expectedKey]?.activity == AgentThreadActivity.READY + presentationModel.snapshot()[expectedKey]?.activityReport?.rowActivity == AgentThreadActivity.READY } } } @@ -1748,7 +1749,7 @@ class AgentSessionRefreshCoordinatorTest { provider = AgentSessionProvider.from("codex"), threadId = "codex-1", title = "Existing title", - activity = AgentThreadActivity.UNREAD, + activityReport = AgentThreadActivityReport(AgentThreadActivity.UNREAD), ) val source = ScriptedSessionSource( @@ -1802,12 +1803,12 @@ class AgentSessionRefreshCoordinatorTest { assertThat(project.threads).hasSize(1) assertThat(project.threads.single().id).isEqualTo("codex-1") assertThat(project.threads.single().title).isEqualTo("Existing title") - assertThat(project.threads.single().activity).isEqualTo(AgentThreadActivity.UNREAD) + assertThat(project.threads.single().activityReport.rowActivity).isEqualTo(AgentThreadActivity.UNREAD) assertThat(project.providerWarnings).hasSize(1) assertThat(project.providerWarnings.single().provider).isEqualTo(AgentSessionProvider.from("codex")) val presentation = presentationModel.snapshot()[presentationKey(PROJECT_PATH, AgentSessionProvider.from("codex"), "codex-1")] assertThat(presentation?.title).isEqualTo("Existing title") - assertThat(presentation?.activity).isEqualTo(AgentThreadActivity.UNREAD) + assertThat(presentation?.activityReport?.rowActivity).isEqualTo(AgentThreadActivity.UNREAD) } } @@ -2580,8 +2581,8 @@ class AgentSessionRefreshCoordinatorTest { ?.threads ?.associateBy { it.id } ?: return@waitForCondition false - threadsById["codex-with-hint"]?.activity == AgentThreadActivity.REVIEWING && - threadsById["codex-without-hint"]?.activity == AgentThreadActivity.UNREAD + threadsById["codex-with-hint"]?.activityReport?.rowActivity == AgentThreadActivity.REVIEWING && + threadsById["codex-without-hint"]?.activityReport?.rowActivity == AgentThreadActivity.UNREAD } } } @@ -2637,7 +2638,7 @@ class AgentSessionRefreshCoordinatorTest { ?.threads ?.firstOrNull { it.id == "claude-1" } ?: return@waitForCondition false - thread.activity == AgentThreadActivity.UNREAD && thread.summaryActivity == AgentThreadActivity.UNREAD + thread.activityReport.rowActivity == AgentThreadActivity.UNREAD && thread.activityReport.chromeActivity == AgentThreadActivity.UNREAD } assertThat(closedRefreshInvocations.get()).isEqualTo(0) @@ -2699,13 +2700,13 @@ class AgentSessionRefreshCoordinatorTest { ?.threads ?.firstOrNull { it.id == "codex-1" } ?: return@waitForCondition false - thread.activity == AgentThreadActivity.PROCESSING && thread.summaryActivity == null && thread.updatedAt == 200L + thread.activityReport.rowActivity == AgentThreadActivity.PROCESSING && thread.activityReport.chromeActivity == null && thread.updatedAt == 200L } val expectedKey = presentationKey(PROJECT_PATH, AgentSessionProvider.from("codex"), "codex-1") waitForCondition { val presentation = presentationModel.snapshot()[expectedKey] ?: return@waitForCondition false - presentation.activity == AgentThreadActivity.PROCESSING && presentation.activityReport.chromeActivity == null && presentation.updatedAt == 200L + presentation.activityReport.rowActivity == AgentThreadActivity.PROCESSING && presentation.activityReport.chromeActivity == null && presentation.updatedAt == 200L } assertThat(closedRefreshInvocations.get()).isEqualTo(0) @@ -2747,7 +2748,7 @@ class AgentSessionRefreshCoordinatorTest { updatedAt = 100L, provider = AgentSessionProvider.from("codex"), activity = AgentThreadActivity.PROCESSING, - summaryActivity = AgentThreadActivity.PROCESSING, + chromeActivity = AgentThreadActivity.PROCESSING, ) ), ) @@ -2776,7 +2777,7 @@ class AgentSessionRefreshCoordinatorTest { ?.threads ?.firstOrNull { it.id == "codex-1" } ?: return@waitForCondition false - thread.activity == AgentThreadActivity.READY && thread.summaryActivity == AgentThreadActivity.READY && thread.updatedAt == 200L + thread.activityReport.rowActivity == AgentThreadActivity.READY && thread.activityReport.chromeActivity == AgentThreadActivity.READY && thread.updatedAt == 200L } assertThat(closedRefreshInvocations.get()).isEqualTo(0) @@ -2853,8 +2854,8 @@ class AgentSessionRefreshCoordinatorTest { ?.threads ?.firstOrNull { it.id == "codex-parent" } ?: return@waitForCondition false - parent.activity == AgentThreadActivity.READY && - parent.summaryActivity == AgentThreadActivity.READY && + parent.activityReport.rowActivity == AgentThreadActivity.READY && + parent.activityReport.chromeActivity == AgentThreadActivity.READY && parent.subAgents.singleOrNull()?.activity == AgentThreadActivity.PROCESSING } @@ -2990,7 +2991,7 @@ class AgentSessionRefreshCoordinatorTest { ?.threads ?.firstOrNull { it.id == "codex-1" } ?: return@waitForCondition false - thread.activity == AgentThreadActivity.READY && thread.summaryActivity == AgentThreadActivity.UNREAD && thread.updatedAt == 300L + thread.activityReport.rowActivity == AgentThreadActivity.READY && thread.activityReport.chromeActivity == AgentThreadActivity.UNREAD && thread.updatedAt == 300L } } } @@ -3071,12 +3072,12 @@ class AgentSessionRefreshCoordinatorTest { ?: return@waitForCondition false closedRefreshInvocations.get() == 1 && thread.updatedAt == 500L && - thread.activity == AgentThreadActivity.UNREAD + thread.activityReport.rowActivity == AgentThreadActivity.UNREAD } val expectedKey = presentationKey(PROJECT_PATH, AgentSessionProvider.from("codex"), "codex-1") waitForCondition { - presentationModel.snapshot()[expectedKey]?.activity == AgentThreadActivity.UNREAD + presentationModel.snapshot()[expectedKey]?.activityReport?.rowActivity == AgentThreadActivity.UNREAD } } } @@ -3100,7 +3101,7 @@ class AgentSessionRefreshCoordinatorTest { updatedAt = 500L, provider = AgentSessionProvider.from("codex"), activity = AgentThreadActivity.READY, - summaryActivity = null, + chromeActivity = null, ) ) } @@ -3139,7 +3140,7 @@ class AgentSessionRefreshCoordinatorTest { updatedAt = 100L, provider = AgentSessionProvider.from("codex"), activity = AgentThreadActivity.READY, - summaryActivity = null, + chromeActivity = null, ) ), ) @@ -3161,8 +3162,8 @@ class AgentSessionRefreshCoordinatorTest { ?.firstOrNull { it.id == "codex-sub-agent" } ?: return@waitForCondition false thread.updatedAt == 500L && - thread.activity == AgentThreadActivity.UNREAD && - thread.summaryActivity == null + thread.activityReport.rowActivity == AgentThreadActivity.UNREAD && + thread.activityReport.chromeActivity == null } } } @@ -3223,7 +3224,8 @@ class AgentSessionRefreshCoordinatorTest { stateStore.snapshot().projects.firstOrNull { it.path == PROJECT_PATH } ?.threads ?.firstOrNull { it.id == "codex-1" } - ?.activity == AgentThreadActivity.UNREAD + ?.activityReport + ?.rowActivity == AgentThreadActivity.UNREAD } } } @@ -3296,7 +3298,8 @@ class AgentSessionRefreshCoordinatorTest { .firstOrNull { it.path == PROJECT_PATH } ?.threads ?.firstOrNull { it.id == "codex-listed" } - ?.activity == AgentThreadActivity.UNREAD + ?.activityReport + ?.rowActivity == AgentThreadActivity.UNREAD } assertThat(capturedRefreshThreadSeedsByPath).isNotEmpty() diff --git a/plugins/agent-workbench/sessions/testSrc/AgentSessionRefreshServiceIntegrationTest.kt b/plugins/agent-workbench/sessions/testSrc/AgentSessionRefreshServiceIntegrationTest.kt index 0856ace66d9c..bfd9c572fafd 100644 --- a/plugins/agent-workbench/sessions/testSrc/AgentSessionRefreshServiceIntegrationTest.kt +++ b/plugins/agent-workbench/sessions/testSrc/AgentSessionRefreshServiceIntegrationTest.kt @@ -408,7 +408,8 @@ class AgentSessionRefreshServiceIntegrationTest { service.state.value.projects.firstOrNull { it.path == PROJECT_PATH } ?.threads ?.singleOrNull() - ?.activity == AgentThreadActivity.PROCESSING + ?.activityReport + ?.rowActivity == AgentThreadActivity.PROCESSING } waitForCondition { @@ -1423,7 +1424,8 @@ class AgentSessionRefreshServiceIntegrationTest { service.state.value.projects.firstOrNull { it.path == PROJECT_PATH } ?.threads ?.firstOrNull() - ?.activity == AgentThreadActivity.UNREAD + ?.activityReport + ?.rowActivity == AgentThreadActivity.UNREAD } service.markThreadAsRead(PROJECT_PATH, AgentSessionProvider.from("claude"), "claude-1", 100) @@ -1432,14 +1434,15 @@ class AgentSessionRefreshServiceIntegrationTest { service.state.value.projects.firstOrNull { it.path == PROJECT_PATH } ?.threads ?.firstOrNull() - ?.activity + ?.activityReport + ?.rowActivity ).isEqualTo(AgentThreadActivity.READY) - assertThat(warmState.getPathSnapshot(PROJECT_PATH)?.threads?.firstOrNull()?.activity) + assertThat(warmState.getPathSnapshot(PROJECT_PATH)?.threads?.firstOrNull()?.activityReport?.rowActivity) .isEqualTo(AgentThreadActivity.READY) val presentationKey = checkNotNull( AgentSessionThreadPresentationKey.create(PROJECT_PATH, AgentSessionProvider.from("claude"), "claude-1") ) - assertThat(service().resolve(presentationKey)?.activity) + assertThat(service().resolve(presentationKey)?.activityReport?.rowActivity) .isEqualTo(AgentThreadActivity.READY) } } @@ -1460,8 +1463,10 @@ class AgentSessionRefreshServiceIntegrationTest { id = "claude-1", updatedAt = 100, provider = AgentSessionProvider.from("claude"), - activity = AgentThreadActivity.PROCESSING, - summaryActivity = AgentThreadActivity.UNREAD, + activityReport = AgentThreadActivityReport( + rowActivity = AgentThreadActivity.PROCESSING, + chromeActivity = AgentThreadActivity.UNREAD, + ), ) ) } @@ -1482,7 +1487,8 @@ class AgentSessionRefreshServiceIntegrationTest { service.state.value.projects.firstOrNull { it.path == PROJECT_PATH } ?.threads ?.firstOrNull() - ?.summaryActivity == AgentThreadActivity.UNREAD + ?.activityReport + ?.chromeActivity == AgentThreadActivity.UNREAD } service.markThreadAsRead(PROJECT_PATH, AgentSessionProvider.from("claude"), "claude-1", 100) @@ -1490,12 +1496,12 @@ class AgentSessionRefreshServiceIntegrationTest { val runtimeThread = service.state.value.projects.firstOrNull { it.path == PROJECT_PATH } ?.threads ?.firstOrNull() - assertThat(runtimeThread?.activity).isEqualTo(AgentThreadActivity.PROCESSING) - assertThat(runtimeThread?.summaryActivity).isEqualTo(AgentThreadActivity.READY) + assertThat(runtimeThread?.activityReport?.rowActivity).isEqualTo(AgentThreadActivity.PROCESSING) + assertThat(runtimeThread?.activityReport?.chromeActivity).isEqualTo(AgentThreadActivity.READY) val warmThread = warmState.getPathSnapshot(PROJECT_PATH)?.threads?.firstOrNull() - assertThat(warmThread?.activity).isEqualTo(AgentThreadActivity.PROCESSING) - assertThat(warmThread?.summaryActivity).isEqualTo(AgentThreadActivity.READY) + assertThat(warmThread?.activityReport?.rowActivity).isEqualTo(AgentThreadActivity.PROCESSING) + assertThat(warmThread?.activityReport?.chromeActivity).isEqualTo(AgentThreadActivity.READY) val presentationKey = checkNotNull(AgentSessionThreadPresentationKey.create(PROJECT_PATH, AgentSessionProvider.from("claude"), "claude-1")) assertThat(service().resolve(presentationKey)?.activityReport) diff --git a/plugins/agent-workbench/sessions/testSrc/AgentSessionRenameServiceTest.kt b/plugins/agent-workbench/sessions/testSrc/AgentSessionRenameServiceTest.kt index a02f0f42a9d7..931c732dc789 100644 --- a/plugins/agent-workbench/sessions/testSrc/AgentSessionRenameServiceTest.kt +++ b/plugins/agent-workbench/sessions/testSrc/AgentSessionRenameServiceTest.kt @@ -94,7 +94,7 @@ class AgentSessionRenameServiceTest { assertThat(stateStore.snapshot().projects.single().threads.single().title).isEqualTo("Renamed thread") val presentation = presentationModel.snapshot()[presentationKey("/work/project", AgentSessionProvider.from("codex"), "thread-1")] assertThat(presentation?.title).isEqualTo("Renamed thread") - assertThat(presentation?.activity).isEqualTo(AgentThreadActivity.PROCESSING) + assertThat(presentation?.activityReport?.rowActivity).isEqualTo(AgentThreadActivity.PROCESSING) assertThat(operationOrder).containsExactly("refresh") assertThat(refreshedPaths).containsExactly("/work/project" to AgentSessionProvider.from("codex")) } diff --git a/plugins/agent-workbench/sessions/testSrc/AgentSessionSleepPreventionServiceTest.kt b/plugins/agent-workbench/sessions/testSrc/AgentSessionSleepPreventionServiceTest.kt index d5453b76606c..b6bf3eb65660 100644 --- a/plugins/agent-workbench/sessions/testSrc/AgentSessionSleepPreventionServiceTest.kt +++ b/plugins/agent-workbench/sessions/testSrc/AgentSessionSleepPreventionServiceTest.kt @@ -2,6 +2,7 @@ package com.intellij.agent.workbench.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.AgentSessionProvider import com.intellij.platform.ai.agent.core.session.AgentSessionThread import com.intellij.agent.workbench.sessions.model.AgentProjectSessions @@ -48,6 +49,34 @@ class AgentSessionSleepPreventionServiceTest { fixture.dispose() } + @Test + fun chromeOnlyWorkingActivityDoesNotKeepSystemAwake() { + val fixture = sleepPreventionFixture() + + fixture.stateFlow.value = sessionsState( + projectThreads = listOf(activeThread(AgentThreadActivity.READY, chromeActivity = AgentThreadActivity.REVIEWING)), + worktreeThreads = listOf(activeThread(AgentThreadActivity.READY, id = "thread-2", chromeActivity = AgentThreadActivity.PROCESSING)), + ) + fixture.service.refreshState() + + assertThat(fixture.inhibitor.acquireCalls).isZero() + assertThat(fixture.inhibitor.releaseCalls).isZero() + fixture.dispose() + } + + @Test + fun rowWorkingActivityKeepsSystemAwakeWhenChromeActivityIsReady() { + val fixture = sleepPreventionFixture() + + fixture.stateFlow.value = sessionsState( + projectThreads = listOf(activeThread(AgentThreadActivity.PROCESSING, chromeActivity = AgentThreadActivity.READY)), + ) + fixture.service.refreshState() + + assertThat(fixture.inhibitor.acquireCalls).isEqualTo(1) + fixture.dispose() + } + @Test fun releaseIsDebouncedUntilLastActiveThreadClears() { val fixture = sleepPreventionFixture() @@ -339,11 +368,15 @@ private fun sessionsState( ) } -private fun activeThread(activity: AgentThreadActivity, id: String = "thread-1") = thread( +private fun activeThread( + rowActivity: AgentThreadActivity, + id: String = "thread-1", + chromeActivity: AgentThreadActivity? = rowActivity, +) = thread( id = id, updatedAt = 1, provider = AgentSessionProvider.from("codex"), - activity = activity, + activityReport = AgentThreadActivityReport(rowActivity = rowActivity, chromeActivity = chromeActivity), ) private class RecordingServiceSleepInhibitor : AgentSleepInhibitor { diff --git a/plugins/agent-workbench/sessions/testSrc/AgentSessionThreadPresentationModelTest.kt b/plugins/agent-workbench/sessions/testSrc/AgentSessionThreadPresentationModelTest.kt index 8d8c5caddd2b..f46ef5e5f213 100644 --- a/plugins/agent-workbench/sessions/testSrc/AgentSessionThreadPresentationModelTest.kt +++ b/plugins/agent-workbench/sessions/testSrc/AgentSessionThreadPresentationModelTest.kt @@ -21,13 +21,13 @@ class AgentSessionThreadPresentationModelTest { provider = AgentSessionProvider.from("codex"), threadId = " thread-1 ", title = " Renamed\n\n thread ", - activity = AgentThreadActivity.PROCESSING, + activityReport = AgentThreadActivityReport(AgentThreadActivity.PROCESSING), ) val key = presentationKey("/work/project", AgentSessionProvider.from("codex"), "thread-1") assertThat(changeSet.changedKeys).containsExactly(key) assertThat(model.snapshot()[key]?.title).isEqualTo("Renamed thread") - assertThat(model.snapshot()[key]?.activity).isEqualTo(AgentThreadActivity.PROCESSING) + assertThat(model.snapshot()[key]?.activityReport?.rowActivity).isEqualTo(AgentThreadActivity.PROCESSING) } @Test @@ -40,7 +40,7 @@ class AgentSessionThreadPresentationModelTest { AgentSessionThreadActivityPresentationUpdate( path = "/work/project", threadId = "thread-1", - activity = AgentThreadActivity.PROCESSING, + activityReport = AgentThreadActivityReport(AgentThreadActivity.PROCESSING), ) ), ) @@ -48,7 +48,7 @@ class AgentSessionThreadPresentationModelTest { val key = presentationKey("/work/project", AgentSessionProvider.from("codex"), "thread-1") assertThat(changeSet.changedKeys).containsExactly(key) assertThat(model.snapshot()[key]?.title).isEmpty() - assertThat(model.snapshot()[key]?.activity).isEqualTo(AgentThreadActivity.PROCESSING) + assertThat(model.snapshot()[key]?.activityReport?.rowActivity).isEqualTo(AgentThreadActivity.PROCESSING) } @Test @@ -110,7 +110,7 @@ class AgentSessionThreadPresentationModelTest { val key = presentationKey("/work/project", AgentSessionProvider.from("codex"), "thread-1") val presentation = model.snapshot()[key] - assertThat(presentation?.activity).isEqualTo(AgentThreadActivity.NEEDS_INPUT) + assertThat(presentation?.activityReport?.rowActivity).isEqualTo(AgentThreadActivity.NEEDS_INPUT) assertThat(presentation?.updatedAt).isEqualTo(100L) } @@ -149,7 +149,7 @@ class AgentSessionThreadPresentationModelTest { val key = presentationKey("/work/project", AgentSessionProvider.from("codex"), "thread-1") val presentation = model.snapshot()[key] assertThat(presentation?.title).isEqualTo("Thread title") - assertThat(presentation?.activity).isEqualTo(AgentThreadActivity.REVIEWING) + assertThat(presentation?.activityReport?.rowActivity).isEqualTo(AgentThreadActivity.REVIEWING) assertThat(presentation?.updatedAt).isEqualTo(200L) } @@ -162,7 +162,7 @@ class AgentSessionThreadPresentationModelTest { provider = AgentSessionProvider.from("codex"), threadId = "removed", title = "Removed", - activity = AgentThreadActivity.READY, + activityReport = AgentThreadActivityReport(AgentThreadActivity.READY), ) val changeSet = model.updateProviderSnapshot( @@ -175,7 +175,7 @@ class AgentSessionThreadPresentationModelTest { "thread-1", "Codex thread", AgentThreadActivity.READY, - summaryActivity = null, + chromeActivity = null, updatedAt = 10L, ), threadModel(AgentSessionProvider.from("claude"), "thread-2", "Claude thread", AgentThreadActivity.PROCESSING), @@ -199,7 +199,7 @@ private fun threadModel( id: String, title: String, activity: AgentThreadActivity, - summaryActivity: AgentThreadActivity? = activity, + chromeActivity: AgentThreadActivity? = activity, updatedAt: Long = 1L, ): AgentSessionThread { return AgentSessionThread( @@ -207,8 +207,7 @@ private fun threadModel( title = title, updatedAt = updatedAt, archived = false, - activity = activity, - summaryActivity = summaryActivity, + activityReport = AgentThreadActivityReport(rowActivity = activity, chromeActivity = chromeActivity), provider = provider, ) } diff --git a/plugins/agent-workbench/sessions/testSrc/AgentSessionWarmStateServiceTest.kt b/plugins/agent-workbench/sessions/testSrc/AgentSessionWarmStateServiceTest.kt index 1f1dc8496a80..55ff833d0672 100644 --- a/plugins/agent-workbench/sessions/testSrc/AgentSessionWarmStateServiceTest.kt +++ b/plugins/agent-workbench/sessions/testSrc/AgentSessionWarmStateServiceTest.kt @@ -1,6 +1,7 @@ package com.intellij.agent.workbench.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.AgentSessionCost import com.intellij.platform.ai.agent.core.session.AgentSessionCostKind import com.intellij.platform.ai.agent.core.session.AgentSessionProvider @@ -55,7 +56,7 @@ class AgentSessionWarmStateServiceTest { assertThat(snapshot?.hasUnknownThreadCount).isTrue() assertThat(snapshot?.threads?.map { it.id }).isEqualTo(listOf("claude-thread", "codex-thread")) assertThat(snapshot?.threads?.map { it.provider }).isEqualTo(listOf(AgentSessionProvider.from("claude"), AgentSessionProvider.from("codex"))) - assertThat(snapshot?.threads?.map { it.activity }).isEqualTo(listOf(AgentThreadActivity.UNREAD, AgentThreadActivity.READY)) + assertThat(snapshot?.threads?.map { it.activityReport.rowActivity }).isEqualTo(listOf(AgentThreadActivity.UNREAD, AgentThreadActivity.READY)) assertThat(snapshot?.threads?.first()?.subAgents?.map { it.id }).isEqualTo(listOf("claude-sub-1")) assertThat(snapshot?.threads?.first()?.originBranch).isEqualTo("feature/x") assertThat(snapshot?.threads?.first()?.cost).isEqualTo( @@ -143,7 +144,7 @@ class AgentSessionWarmStateServiceTest { assertThat(snapshot?.hasUnknownThreadCount).isTrue() assertThat(snapshot?.threads?.map { it.id }).isEqualTo(listOf("claude-thread", "codex-thread")) assertThat(snapshot?.threads?.map { it.provider }).isEqualTo(listOf(AgentSessionProvider.from("claude"), AgentSessionProvider.from("codex"))) - assertThat(snapshot?.threads?.map { it.activity }).isEqualTo(listOf(AgentThreadActivity.UNREAD, AgentThreadActivity.READY)) + assertThat(snapshot?.threads?.map { it.activityReport.rowActivity }).isEqualTo(listOf(AgentThreadActivity.UNREAD, AgentThreadActivity.READY)) assertThat(snapshot?.threads?.first()?.cost).isEqualTo( AgentSessionCost( amountUsd = BigDecimal("2.50"), @@ -154,7 +155,7 @@ class AgentSessionWarmStateServiceTest { } @Test - fun warmStateRoundTripPreservesNonContributingSummaryActivity() { + fun warmStateRoundTripPreservesNonContributingChromeActivity() { val original = AgentSessionWarmStateService() original.setPathSnapshot( "/work/project-a/", @@ -164,8 +165,7 @@ class AgentSessionWarmStateServiceTest { id = "codex-sub-agent", updatedAt = 10, provider = AgentSessionProvider.from("codex"), - activity = AgentThreadActivity.UNREAD, - summaryActivity = null, + activityReport = AgentThreadActivityReport(rowActivity = AgentThreadActivity.UNREAD, chromeActivity = null), ), ), updatedAt = 200, @@ -176,7 +176,7 @@ class AgentSessionWarmStateServiceTest { reloaded.loadState(original.state) val thread = reloaded.getPathSnapshot("/work/project-a")?.threads?.single() - assertThat(thread?.activity).isEqualTo(AgentThreadActivity.UNREAD) - assertThat(thread?.summaryActivity).isNull() + assertThat(thread?.activityReport?.rowActivity).isEqualTo(AgentThreadActivity.UNREAD) + assertThat(thread?.activityReport?.chromeActivity).isNull() } } diff --git a/plugins/agent-workbench/sessions/testSrc/model/AgentSessionThreadOrderingTest.kt b/plugins/agent-workbench/sessions/testSrc/model/AgentSessionThreadOrderingTest.kt index 36bf503d485a..4780e3a57972 100644 --- a/plugins/agent-workbench/sessions/testSrc/model/AgentSessionThreadOrderingTest.kt +++ b/plugins/agent-workbench/sessions/testSrc/model/AgentSessionThreadOrderingTest.kt @@ -2,6 +2,7 @@ package com.intellij.agent.workbench.sessions.model 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 org.assertj.core.api.Assertions.assertThat @@ -109,7 +110,7 @@ class AgentSessionThreadOrderingTest { title = title, updatedAt = updatedAt, archived = false, - activity = activity, + activityReport = AgentThreadActivityReport(activity), provider = provider, ) } diff --git a/plugins/agent-workbench/spec/sessions/agent-sessions-codex-activity.spec.md b/plugins/agent-workbench/spec/sessions/agent-sessions-codex-activity.spec.md index 1adfe606c0ab..8ce223d62b33 100644 --- a/plugins/agent-workbench/spec/sessions/agent-sessions-codex-activity.spec.md +++ b/plugins/agent-workbench/spec/sessions/agent-sessions-codex-activity.spec.md @@ -57,7 +57,7 @@ Workbench shows normalized activity (`NEEDS_INPUT`, `UNREAD`, `REVIEWING`, `PROC [@test] ../../lib-agent/providers/codex/sessions/testSrc/CodexSessionSourceRolloutIntegrationTest.kt - Thread-scoped app-server refresh for a grouped sub-agent child must return the folded parent thread. Partial parent updates merge returned sub-agents by id with existing siblings so one child status update cannot drop other children. - Folded sub-agent activity is rendered only on the sub-agent tree row. It must not contribute to parent row activity, parent summary activity, tool-window counters, stripe badges, activity menu rows, or OS notifications. + Folded sub-agent activity is rendered only on the sub-agent tree row. It must not contribute to parent row activity, parent chrome activity, tool-window counters, stripe badges, activity menu rows, or OS notifications. [@test] ../../lib-agent/providers/codex/sessions/testSrc/CodexAppServerSessionBackendTest.kt [@test] ../../sessions/testSrc/AgentSessionRefreshCoordinatorTest.kt diff --git a/plugins/agent-workbench/spec/sessions/agent-sessions-pi.spec.md b/plugins/agent-workbench/spec/sessions/agent-sessions-pi.spec.md index 100fabdbbb18..67ed7f89df5d 100644 --- a/plugins/agent-workbench/spec/sessions/agent-sessions-pi.spec.md +++ b/plugins/agent-workbench/spec/sessions/agent-sessions-pi.spec.md @@ -7,8 +7,11 @@ targets: - ../../lib-agent/providers/pi/sessions-filewatch/resources/intellij.platform.ai.agent.pi.sessions.filewatch.xml - ../../lib-agent/providers/pi/sessions/resources/pi-extension/agent-workbench-extension.ts - ../../lib-agent/providers/pi/sessions/resources/pi-extension/control.ts + - ../../lib-agent/providers/pi/sessions/resources/pi-extension/taskFolders.ts - ../../lib-agent/providers/pi/sessions/src/**/*.kt - ../../lib-agent/providers/pi/sessions/testSrc/*.kt + - ../../pi/awb/resources/intellij.agent.workbench.pi.awb.xml + - ../../pi/awb/src/PiTaskFolderControlHandler.kt - ../../lib-agent/sessions-core/src/providers/AgentSessionProviderDescriptor.kt - ../../sessions/src/settings/*.kt - ../../sessions/testSrc/settings/*.kt @@ -90,13 +93,15 @@ Agent Workbench treats Pi as a first-class terminal-backed provider. Pi sessions - The bundled Pi extension must open one private IDE-local WebSocket control connection for live Structure View actions. The WebSocket handshake must be authenticated with `Authorization: Bearer `, the first `hello` frame must bind the connection to the same launch-scoped token/session id/cwd, and every command must target the currently bound session id. The IDE may send `navigateTree` and `forkFromEntry` commands only when the live connection advertises those capabilities. Fork commands must use Pi `fork(entryId, { position: "at", withSession })` and return the fresh replacement session state captured from `withSession`; there is no long-polling, polling, slash-command, or unauthenticated fallback. [@test] ../../lib-agent/providers/pi/sessions/testSrc/PiExtensionControlWebSocketHandlerTest.kt -- The same authenticated control WebSocket may carry explicit task-folder capability requests from the bundled Pi extension to the IDE using - `taskFolderRequest` frames with an `operation` string and nested `arguments` object. Supported operations cover current-folder lookup, - folder listing, thread assignment listing, create-and-assign, current-thread assign/unassign, rename, metadata set/delete, mark done, - and delete. These requests must use the bound cwd/session id to resolve current-session context, use global folder id for explicit - folder mutations, must not inject prompt context automatically, and must return normal `response` frames with `ok`, `requestId`, and a - `result` object containing the requested folder, assignment, metadata, or mutation fields. Metadata is ordinary string key/value data; - `issue` and `review` are conventional keys, not separate protocol fields. +- The same authenticated control WebSocket may carry explicit Agent Workbench task-folder capability requests from the bundled Pi extension's task-folder tools + to the IDE using `taskFolderRequest` frames with an `operation` string and nested `arguments` object. The base Pi sessions module must own + only authenticated transport, shared parsing, and extension-request routing; the task-folder request handler lives in the AWB Pi module and + depends on the AWB task-folder service. Supported operations cover current-folder lookup, folder listing, thread assignment listing, + create-and-assign, current-thread assign/unassign, rename, metadata set/delete, mark done, and delete. These requests must use the bound + cwd/session id to resolve current-session context, use global folder id for explicit folder mutations, must not inject prompt context + automatically, and must return normal `response` frames with `ok`, `requestId`, and a `result` object containing the requested folder, + assignment, metadata, or mutation fields. Metadata is ordinary string key/value data; `issue` and `review` are conventional keys, not + separate protocol fields. [@test] ../../lib-agent/providers/pi/sessions/testSrc/PiExtensionControlWebSocketHandlerTest.kt [@test] ../../lib-agent/providers/pi/sessions/testSrc/PiThemeSupportTest.kt diff --git a/plugins/agent-workbench/spec/sessions/agent-sessions-refresh.spec.md b/plugins/agent-workbench/spec/sessions/agent-sessions-refresh.spec.md index 7855d464fd3f..bb710364c1d9 100644 --- a/plugins/agent-workbench/spec/sessions/agent-sessions-refresh.spec.md +++ b/plugins/agent-workbench/spec/sessions/agent-sessions-refresh.spec.md @@ -70,11 +70,11 @@ Session refresh is event-driven and provider-agnostic. It merges provider result concurrent project loads cannot observe or prune each other's partial rescan state. [@test] ../../lib-agent/providers/claude/sessions/testSrc/ClaudeStoreSessionBackendTest.kt -- Provider refresh publishes shared thread presentation keyed by normalized path and canonical thread identity so Agent Threads and open editor tabs show the same title/activity. Agent Threads activity counters use canonical thread summary activity; shared presentation may refresh titles without changing counter buckets. +- Provider refresh publishes shared thread presentation keyed by normalized path and canonical thread identity so Agent Threads and open editor tabs show the same title/activity report. Agent Threads rows use canonical row activity, while activity counters use canonical chrome activity; shared presentation may refresh titles without changing counter buckets. [@test] ../../sessions/testSrc/AgentSessionThreadPresentationTest.kt [@test] ../../chat/testSrc/AgentChatEditorServiceTest.kt -- Activity and presentation updates carry evidence authority. Provisional provider signals may update row activity, but must not clear an existing attention-bucket chrome/summary activity. Semantic provider signals and authoritative snapshots may clear attention when they explicitly report a non-attention activity. +- Activity and presentation updates carry evidence authority. Provisional provider signals may update row activity, but must not clear an existing attention-bucket chrome activity. Semantic provider signals and authoritative snapshots may clear attention when they explicitly report a non-attention activity. [@test] ../../sessions/testSrc/service/AgentSessionRefreshSchedulerTest.kt - File-backed provider watchers must recover after watch-loop failures while the owning watcher remains active. diff --git a/plugins/agent-workbench/spec/sessions/agent-sessions-sleep-prevention.spec.md b/plugins/agent-workbench/spec/sessions/agent-sessions-sleep-prevention.spec.md index 1015635cd761..c616ea43915b 100644 --- a/plugins/agent-workbench/spec/sessions/agent-sessions-sleep-prevention.spec.md +++ b/plugins/agent-workbench/spec/sessions/agent-sessions-sleep-prevention.spec.md @@ -45,8 +45,8 @@ IDE Power Save Mode is a hard override: while it is enabled, sleep prevention mu Background projects and worktrees count; behavior must not be limited to the selected tree row, selected tab, or focused project. [@test] ../../sessions/testSrc/AgentSessionSleepPreventionServiceTest.kt -- Aggregate working state must be `true` if and only if any loaded thread in any project or worktree has normalized activity `PROCESSING` or `REVIEWING`. - Threads with `READY` or `UNREAD` must not keep the blocker held. +- Aggregate working state must be `true` if and only if any loaded thread in any project or worktree has canonical row activity `PROCESSING` or `REVIEWING` (`AgentSessionThread.activityReport.rowActivity`). + Threads with row activity `READY` or `UNREAD` must not keep the blocker held, even when their chrome activity is non-ready. [@test] ../../sessions/testSrc/AgentSessionSleepPreventionServiceTest.kt - When the setting is enabled and aggregate working state becomes active, the service must acquire sleep prevention immediately with no debounce. @@ -97,7 +97,7 @@ IDE Power Save Mode is a hard override: while it is enabled, sleep prevention mu - No new tool-window badge, notification, or status text is required for this feature. ## Data & Backend -- Activity aggregation must consume normalized `AgentThreadActivity` values already present in sessions state. +- Activity aggregation must consume normalized `AgentThreadActivity` row values already present in sessions state. - The service may observe `AgentSessionReadService.stateFlow()` or the underlying app-level sessions state store, but behavior must stay event-driven; polling loops are not allowed. - The service must also observe IDE Power Save Mode changes and treat Power Save Mode as a hard override on blocker ownership. - The 30-second debounce applies only to releasing an already-held blocker. Acquisition is always immediate. diff --git a/plugins/agent-workbench/spec/sessions/agent-sessions-tree.spec.md b/plugins/agent-workbench/spec/sessions/agent-sessions-tree.spec.md index 40fbdbb14073..c49ce9f30603 100644 --- a/plugins/agent-workbench/spec/sessions/agent-sessions-tree.spec.md +++ b/plugins/agent-workbench/spec/sessions/agent-sessions-tree.spec.md @@ -30,7 +30,7 @@ The Agent Threads tree follows IntelliJ tree conventions while adding provider-s - The tool window surfaces thread activity through chrome rather than a body strip: the title bar carries one counter action per active bucket (`Needs attention` covers `NEEDS_INPUT`/`REVIEWING`, `Running` covers `PROCESSING`, `Done` covers `UNREAD`). `READY` threads are not surfaced in title chrome. - Thread rows render their actual activity badge, but title counters, stripe badges, and OS notifications use summary activity; + Thread rows render their canonical row activity badge, but title counters, stripe badges, and OS notifications use chrome activity; shared presentation may update row titles but must not override activity for those counters. Sub-agent-only activity does not contribute to those global signals. Title counters and stripe badges ignore activity rows whose thread `updatedAt` is older than 3 days; the tree and OS notification transition tracking still use the full active thread state. diff --git a/plugins/agent-workbench/spec/sessions/agent-sessions.spec.md b/plugins/agent-workbench/spec/sessions/agent-sessions.spec.md index da73836586e3..522b323845bc 100644 --- a/plugins/agent-workbench/spec/sessions/agent-sessions.spec.md +++ b/plugins/agent-workbench/spec/sessions/agent-sessions.spec.md @@ -64,7 +64,7 @@ behavior; refresh mechanics and detailed tree rendering/interaction contracts li [@test] ../../sessions-actions/testSrc/AgentSessionsMainToolbarNewThreadActionsTest.kt - Task-folder grouping, lifecycle actions, explicit metadata, and folder tree placement must follow `agent-task-folders.spec.md`. - [@test] ../../sessions/testSrc/AgentTaskFolderServiceTest.kt + [@test] ../../sessions-task-folders/testSrc/AgentTaskFolderServiceTest.kt [@test] ../../sessions-toolwindow/testSrc/AgentSessionsTreeSnapshotTest.kt [@test] ../../sessions-toolwindow/testSrc/AgentSessionsTreePopupActionsTest.kt diff --git a/plugins/agent-workbench/spec/sessions/agent-task-folders.spec.md b/plugins/agent-workbench/spec/sessions/agent-task-folders.spec.md index 22d2a25924e4..ab49f24c3246 100644 --- a/plugins/agent-workbench/spec/sessions/agent-task-folders.spec.md +++ b/plugins/agent-workbench/spec/sessions/agent-task-folders.spec.md @@ -2,17 +2,17 @@ name: Agent Task Folders description: Requirements for task folders that group Agent Threads work without changing the one-thread-one-session model. targets: - - ../../lib-agent/sessions-core/src/folders/AgentTaskFolders.kt - - ../../lib-agent/sessions-core/src/SessionActionTarget.kt + - ../../sessions-task-folders/src/AgentTaskFolders.kt - ../../sessions/src/service/AgentSessionArchiveService.kt - ../../sessions/resources/messages/AgentSessionsBundle.properties - ../../sessions-toolwindow/src/tree/SessionTree.kt - ../../sessions-toolwindow/src/tree/SessionTreeSearch.kt - ../../sessions-toolwindow/src/ui/AgentSessionsTree*.kt + - ../../sessions-toolwindow/src/actions/AgentSessionsTreePopupActions.kt - ../../sessions-toolwindow/src/actions/*TaskFolder*.kt - ../../sessions-toolwindow/src/actions/SessionTreeActionTargets.kt - ../../sessions-toolwindow/resources/intellij.agent.workbench.sessions.toolwindow.xml - - ../../sessions/testSrc/AgentTaskFolderServiceTest.kt + - ../../sessions-task-folders/testSrc/AgentTaskFolderServiceTest.kt - ../../sessions-toolwindow/testSrc/AgentSessionsTreeSnapshotTest.kt - ../../sessions-toolwindow/testSrc/AgentSessionsTreePopupActionsTest.kt --- @@ -33,12 +33,12 @@ create a new top-level Agent Workbench entity and do not inject context into pro - The folder service must persist app-level, non-roamable folder state keyed by global folder id. New folder ids must be generated as platform KSUIDs; persisted folder ids must be trimmed and accepted only if they match KSUID shape. Folder path associations, names, assignment thread ids, and metadata keys must be trimmed or normalized; invalid or orphaned persisted records must be dropped on load. - [@test] ../../sessions/testSrc/AgentTaskFolderServiceTest.kt + [@test] ../../sessions-task-folders/testSrc/AgentTaskFolderServiceTest.kt - A thread may be assigned to at most one in-progress task folder per normalized path/provider/thread id. Assigning a thread to another folder must move it from the previous folder. Folder rename, metadata, delete, and status mutations must address the global folder id, not the render path. Done folders must be hidden from the active tree and reject new assignments. - [@test] ../../sessions/testSrc/AgentTaskFolderServiceTest.kt + [@test] ../../sessions-task-folders/testSrc/AgentTaskFolderServiceTest.kt - The active Agent Threads tree must render in-progress task folders under their owning project or worktree. Loaded assigned threads must appear as children of the folder and must be removed from the normal ungrouped thread rows and `More` counts. Assigned threads that are @@ -74,6 +74,6 @@ create a new top-level Agent Workbench entity and do not inject context into pro ## Testing / Local Run -- `./tests.cmd --module intellij.agent.workbench.sessions.tests --test com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderServiceTest` +- `./tests.cmd --module intellij.agent.workbench.sessions.task.folders.tests --test com.intellij.agent.workbench.sessions.task.folders.AgentTaskFolderServiceTest` - `./tests.cmd --module intellij.agent.workbench.sessions.toolwindow.tests --test com.intellij.agent.workbench.sessions.toolwindow.AgentSessionsTreeSnapshotTest` - `./tests.cmd --module intellij.agent.workbench.sessions.toolwindow.tests --test com.intellij.agent.workbench.sessions.toolwindow.AgentSessionsTreePopupActionsTest`