From 019a5880bbce17ddc07c102a91649ef7053b8ddf Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Wed, 1 Jul 2026 13:59:00 +0200 Subject: [PATCH] IJPL-248623 move Agent Workbench modules into AIR Move the remaining Agent Workbench shared libraries, prompt core, and sessions core modules out of the community agent-workbench tree and into the AIR plugin layout. The shared utility modules now live under plugins/air/shared, while prompt and sessions modules are packaged with the rest of AIR. Update module names, package prefixes, plugin descriptors, API dumps, specs, Patronus rules, and generated Bazel/JPS metadata so product packaging and project-structure checks use the AIR locations directly. IJ-MR-184958 IJ-MR-184993 IJ-MR-179029 IJ-MR-184126 IJ-MR-181153 IJ-MR-146078 IJ-MR-175479 IJ-MR-186058 IJ-MR-193195 IJ-MR-196957 IJ-MR-199124 IJ-MR-197441 IJ-MR-204135 IJ-MR-204674 IJ-MR-205883 IJ-MR-208539 GitOrigin-RevId: a389389e072d4f58b38cda84156b3e1fbd445ad3 --- .idea/modules.xml | 13 - BUILD.bazel | 12 - build/bazel-generated-file-list.txt | 7 - intellij.idea.community.main.tests.iml | 6 - .../intellij/build/impl/TestingTasksImpl.kt | 6 - .../agent-workbench/lib-agent/cli/BUILD.bazel | 133 ---- .../lib-agent/cli/api-dump.txt | 0 .../cli/intellij.platform.ai.agent.cli.iml | 19 - .../intellij.platform.ai.agent.cli.tests.iml | 21 - .../intellij.platform.ai.agent.cli.xml | 7 - .../cli/src/AgentCliExecutableResolver.kt | 128 --- .../testSrc/AgentCliExecutableResolverTest.kt | 210 ----- .../lib-agent/common/BUILD.bazel | 128 --- .../lib-agent/common/api-dump.txt | 36 - .../common/exposed-third-party-api.txt | 1 - .../intellij.platform.ai.agent.common.iml | 20 - ...ntellij.platform.ai.agent.common.tests.iml | 19 - .../common/resources/icons/claude.svg | 3 - .../common/resources/icons/claudeGray.svg | 3 - .../resources/icons/claudeGray_dark.svg | 3 - .../common/resources/icons/codex.svg | 40 - .../common/resources/icons/codexGray.svg | 5 - .../common/resources/icons/codexGray_dark.svg | 5 - .../common/resources/icons/junie.svg | 6 - .../common/resources/icons/junieGray.svg | 6 - .../common/resources/icons/junieGray_dark.svg | 6 - .../common/resources/icons/opencode.svg | 7 - .../common/resources/icons/opencodeGray.svg | 7 - .../resources/icons/opencodeGray_dark.svg | 7 - .../lib-agent/common/resources/icons/pi.svg | 5 - .../common/resources/icons/pi_dark.svg | 5 - .../intellij.platform.ai.agent.common.xml | 7 - .../common/src/AgentThreadActivityIcons.kt | 11 - .../src/AgentThreadActivityPresentation.kt | 106 --- .../src/icons/AgentWorkbenchCommonIcons.java | 26 - .../AgentThreadActivityPresentationTest.kt | 94 --- .../lib-agent/core/BUILD.bazel | 120 --- .../lib-agent/core/api-dump.txt | 256 ------ .../core/exposed-third-party-api.txt | 2 - .../core/intellij.platform.ai.agent.core.iml | 16 - .../intellij.platform.ai.agent.core.tests.iml | 19 - .../intellij.platform.ai.agent.core.xml | 2 - .../lib-agent/core/src/AgentThreadActivity.kt | 51 -- .../lib-agent/core/src/AgentThreadIdentity.kt | 38 - .../core/src/AgentWorkbenchPathUtil.kt | 40 - .../AgentWorkbenchExtensionPointSupport.kt | 122 --- .../src/session/AgentSessionArchiveTitles.kt | 38 - .../core/src/session/AgentSessionCost.kt | 16 - .../core/src/session/AgentSessionModels.kt | 288 ------- .../testSrc/AgentSessionArchiveTitlesTest.kt | 34 - .../testSrc/AgentSessionOutlineTreeTest.kt | 59 -- .../core/testSrc/AgentThreadIdentityTest.kt | 40 - .../testSrc/AgentWorkbenchPathUtilTest.kt | 46 -- .../lib-agent/filewatch/BUILD.bazel | 115 --- .../lib-agent/filewatch/api-dump.txt | 37 - .../filewatch/exposed-third-party-api.txt | 1 - .../intellij.platform.ai.agent.filewatch.iml | 16 - ...llij.platform.ai.agent.filewatch.tests.iml | 17 - .../intellij.platform.ai.agent.filewatch.xml | 2 - .../src/AgentWorkbenchDirectoryWatcher.kt | 205 ----- .../filewatch/src/ImmediateFileChangeFlow.kt | 224 ------ .../src/impl/DirectoryChangeEvent.kt | 19 - .../src/impl/DirectoryChangeListener.kt | 9 - .../filewatch/src/impl/DirectoryWatcher.kt | 211 ----- .../filewatch/src/impl/FileTreeVisitor.kt | 28 - .../lib-agent/filewatch/src/impl/PathUtils.kt | 18 - .../impl/watchservice/AbstractWatchService.kt | 177 ----- .../MacOSXListeningWatchService.kt | 364 --------- .../impl/watchservice/jna/MacFSEventsApi.kt | 90 --- .../src/impl/watchservice/jna/MacKQueueApi.kt | 62 -- .../AgentWorkbenchDirectoryWatcherTest.kt | 267 ------- .../testSrc/impl/DirectoryWatcherImplTest.kt | 424 ---------- .../MacOSXListeningWatchServiceTest.kt | 169 ---- .../lib-agent/json/BUILD.bazel | 132 ---- .../lib-agent/json/api-dump.txt | 97 --- .../json/exposed-third-party-api.txt | 5 - .../json/intellij.platform.ai.agent.json.iml | 20 - .../intellij.platform.ai.agent.json.tests.iml | 21 - .../intellij.platform.ai.agent.json.xml | 7 - .../json/src/WorkbenchJsonParsingUtils.kt | 93 --- .../json/src/WorkbenchJsonlScanner.kt | 182 ----- .../filebacked/FileBackedSessionChangeSet.kt | 28 - .../FileBackedSessionEventClassifier.kt | 44 -- .../FileBackedSessionInvalidationState.kt | 147 ---- .../FileBackedSessionUpdatesFlow.kt | 51 -- .../filebacked/FileBackedSessionWatcher.kt | 78 -- .../FileBackedSessionInvalidationStateTest.kt | 121 --- .../FileBackedSessionUpdatesFlowTest.kt | 73 -- .../lib-agent/sessions-core/BUILD.bazel | 74 -- .../lib-agent/sessions-core/api-dump.txt | 740 ------------------ .../sessions-core/exposed-third-party-api.txt | 5 - ...tellij.platform.ai.agent.sessions.core.iml | 48 -- ...tellij.platform.ai.agent.sessions.core.xml | 72 -- .../src/AgentSessionThreadIds.kt | 13 - .../src/AgentSessionThreadPresentation.kt | 90 --- .../AgentSessionThreadPresentationModel.kt | 316 -------- .../src/AgentSessionThreadRebindPolicy.kt | 36 - .../sessions-core/src/SessionActionTarget.kt | 54 -- ...ntWorkbenchProjectRuntimeConfigProvider.kt | 38 - .../src/cost/AgentSessionUsageSnapshot.kt | 63 -- .../launch/AgentSessionLaunchContributor.kt | 95 --- .../src/launch/AgentSessionLaunchPlanner.kt | 189 ----- .../launch/AgentSessionLaunchSpecAugmenter.kt | 56 -- .../src/launch/AgentSessionLaunchSpecs.kt | 93 --- .../src/launch/AgentSessionOutOfBandLaunch.kt | 49 -- .../src/launch/AgentSessionSurfaces.kt | 97 --- .../launch/AgentSessionThreadViewOpenPlan.kt | 67 -- .../src/launch/AwbMcpConfigBuilder.kt | 260 ------ .../src/launch/AwbMcpConfigContributor.kt | 51 -- .../launch/AwbMcpConfigProviderContributor.kt | 77 -- .../src/launch/McpStreamUrlProvider.kt | 32 - .../src/paths/AgentWorkbenchProjectPaths.kt | 79 -- .../AgentOpenTopLevelThreadDispatchService.kt | 14 - .../providers/AgentPromptProviderOption.kt | 47 -- .../providers/AgentSessionLaunchProfiles.kt | 248 ------ .../AgentSessionProviderDescriptor.kt | 607 -------------- .../AgentSessionProviderFeatureSettings.kt | 25 - .../AgentSessionProviderMenuModel.kt | 83 -- .../providers/AgentSessionProviderPlanMode.kt | 50 -- .../AgentSessionProviderUiContributor.kt | 56 -- .../src/providers/AgentSessionProviders.kt | 148 ---- .../src/providers/AgentSessionSource.kt | 702 ----------------- .../src/providers/BaseAgentSessionSource.kt | 146 ---- .../src/util/JbCentralCliSupport.kt | 82 -- .../agent-workbench/prompt/core/BUILD.bazel | 187 ----- .../intellij.agent.workbench.prompt.core.iml | 54 -- ...llij.agent.workbench.prompt.core.tests.iml | 55 -- .../intellij.agent.workbench.prompt.core.xml | 47 -- .../AgentPromptSuggestionsBundle.properties | 30 - .../src/AgentPromptBuiltinContextRenderers.kt | 303 ------- .../core/src/AgentPromptContainerLauncher.kt | 21 - .../AgentPromptContextContributorBridge.kt | 76 -- .../AgentPromptContextEnvelopeFormatter.kt | 341 -------- .../core/src/AgentPromptContextPayloads.kt | 51 -- .../src/AgentPromptContextRendererBridge.kt | 131 ---- .../src/AgentPromptContextResolverService.kt | 67 -- .../AgentPromptInvocationDataAttributes.kt | 25 - .../core/src/AgentPromptLauncherBridge.kt | 102 --- .../AgentPromptManualContextSourceBridge.kt | 49 -- .../prompt/core/src/AgentPromptModels.kt | 234 ------ .../core/src/AgentPromptPaletteExtension.kt | 113 --- .../core/src/AgentPromptProjectPathContext.kt | 20 - .../src/AgentPromptReusableSourceEntry.kt | 21 - .../core/src/AgentPromptSuggestionBundle.kt | 19 - .../src/AgentPromptSuggestionGenerator.kt | 296 ------- .../core/src/AgentPromptSuggestionSeeds.kt | 84 -- .../AgentPromptSuggestionSessionService.kt | 312 -------- ...AgentPromptContextEnvelopeFormatterTest.kt | 126 --- .../AgentPromptContextResolverServiceTest.kt | 183 ----- ...entPromptDefaultSuggestionGeneratorTest.kt | 112 --- .../AgentPromptSuggestionGeneratorTest.kt | 194 ----- ...AgentPromptSuggestionSessionServiceTest.kt | 550 ------------- 152 files changed, 14564 deletions(-) delete mode 100644 plugins/agent-workbench/lib-agent/cli/BUILD.bazel delete mode 100644 plugins/agent-workbench/lib-agent/cli/api-dump.txt delete mode 100644 plugins/agent-workbench/lib-agent/cli/intellij.platform.ai.agent.cli.iml delete mode 100644 plugins/agent-workbench/lib-agent/cli/intellij.platform.ai.agent.cli.tests.iml delete mode 100644 plugins/agent-workbench/lib-agent/cli/resources/intellij.platform.ai.agent.cli.xml delete mode 100644 plugins/agent-workbench/lib-agent/cli/src/AgentCliExecutableResolver.kt delete mode 100644 plugins/agent-workbench/lib-agent/cli/testSrc/AgentCliExecutableResolverTest.kt delete mode 100644 plugins/agent-workbench/lib-agent/common/BUILD.bazel delete mode 100644 plugins/agent-workbench/lib-agent/common/api-dump.txt delete mode 100644 plugins/agent-workbench/lib-agent/common/exposed-third-party-api.txt delete mode 100644 plugins/agent-workbench/lib-agent/common/intellij.platform.ai.agent.common.iml delete mode 100644 plugins/agent-workbench/lib-agent/common/intellij.platform.ai.agent.common.tests.iml delete mode 100644 plugins/agent-workbench/lib-agent/common/resources/icons/claude.svg delete mode 100644 plugins/agent-workbench/lib-agent/common/resources/icons/claudeGray.svg delete mode 100644 plugins/agent-workbench/lib-agent/common/resources/icons/claudeGray_dark.svg delete mode 100644 plugins/agent-workbench/lib-agent/common/resources/icons/codex.svg delete mode 100644 plugins/agent-workbench/lib-agent/common/resources/icons/codexGray.svg delete mode 100644 plugins/agent-workbench/lib-agent/common/resources/icons/codexGray_dark.svg delete mode 100644 plugins/agent-workbench/lib-agent/common/resources/icons/junie.svg delete mode 100644 plugins/agent-workbench/lib-agent/common/resources/icons/junieGray.svg delete mode 100644 plugins/agent-workbench/lib-agent/common/resources/icons/junieGray_dark.svg delete mode 100644 plugins/agent-workbench/lib-agent/common/resources/icons/opencode.svg delete mode 100644 plugins/agent-workbench/lib-agent/common/resources/icons/opencodeGray.svg delete mode 100644 plugins/agent-workbench/lib-agent/common/resources/icons/opencodeGray_dark.svg delete mode 100644 plugins/agent-workbench/lib-agent/common/resources/icons/pi.svg delete mode 100644 plugins/agent-workbench/lib-agent/common/resources/icons/pi_dark.svg delete mode 100644 plugins/agent-workbench/lib-agent/common/resources/intellij.platform.ai.agent.common.xml delete mode 100644 plugins/agent-workbench/lib-agent/common/src/AgentThreadActivityIcons.kt delete mode 100644 plugins/agent-workbench/lib-agent/common/src/AgentThreadActivityPresentation.kt delete mode 100644 plugins/agent-workbench/lib-agent/common/src/icons/AgentWorkbenchCommonIcons.java delete mode 100644 plugins/agent-workbench/lib-agent/common/testSrc/AgentThreadActivityPresentationTest.kt delete mode 100644 plugins/agent-workbench/lib-agent/core/BUILD.bazel delete mode 100644 plugins/agent-workbench/lib-agent/core/api-dump.txt delete mode 100644 plugins/agent-workbench/lib-agent/core/exposed-third-party-api.txt delete mode 100644 plugins/agent-workbench/lib-agent/core/intellij.platform.ai.agent.core.iml delete mode 100644 plugins/agent-workbench/lib-agent/core/intellij.platform.ai.agent.core.tests.iml delete mode 100644 plugins/agent-workbench/lib-agent/core/resources/intellij.platform.ai.agent.core.xml delete mode 100644 plugins/agent-workbench/lib-agent/core/src/AgentThreadActivity.kt delete mode 100644 plugins/agent-workbench/lib-agent/core/src/AgentThreadIdentity.kt delete mode 100644 plugins/agent-workbench/lib-agent/core/src/AgentWorkbenchPathUtil.kt delete mode 100644 plugins/agent-workbench/lib-agent/core/src/extensions/AgentWorkbenchExtensionPointSupport.kt delete mode 100644 plugins/agent-workbench/lib-agent/core/src/session/AgentSessionArchiveTitles.kt delete mode 100644 plugins/agent-workbench/lib-agent/core/src/session/AgentSessionCost.kt delete mode 100644 plugins/agent-workbench/lib-agent/core/src/session/AgentSessionModels.kt delete mode 100644 plugins/agent-workbench/lib-agent/core/testSrc/AgentSessionArchiveTitlesTest.kt delete mode 100644 plugins/agent-workbench/lib-agent/core/testSrc/AgentSessionOutlineTreeTest.kt delete mode 100644 plugins/agent-workbench/lib-agent/core/testSrc/AgentThreadIdentityTest.kt delete mode 100644 plugins/agent-workbench/lib-agent/core/testSrc/AgentWorkbenchPathUtilTest.kt delete mode 100644 plugins/agent-workbench/lib-agent/filewatch/BUILD.bazel delete mode 100644 plugins/agent-workbench/lib-agent/filewatch/api-dump.txt delete mode 100644 plugins/agent-workbench/lib-agent/filewatch/exposed-third-party-api.txt delete mode 100644 plugins/agent-workbench/lib-agent/filewatch/intellij.platform.ai.agent.filewatch.iml delete mode 100644 plugins/agent-workbench/lib-agent/filewatch/intellij.platform.ai.agent.filewatch.tests.iml delete mode 100644 plugins/agent-workbench/lib-agent/filewatch/resources/intellij.platform.ai.agent.filewatch.xml delete mode 100644 plugins/agent-workbench/lib-agent/filewatch/src/AgentWorkbenchDirectoryWatcher.kt delete mode 100644 plugins/agent-workbench/lib-agent/filewatch/src/ImmediateFileChangeFlow.kt delete mode 100644 plugins/agent-workbench/lib-agent/filewatch/src/impl/DirectoryChangeEvent.kt delete mode 100644 plugins/agent-workbench/lib-agent/filewatch/src/impl/DirectoryChangeListener.kt delete mode 100644 plugins/agent-workbench/lib-agent/filewatch/src/impl/DirectoryWatcher.kt delete mode 100644 plugins/agent-workbench/lib-agent/filewatch/src/impl/FileTreeVisitor.kt delete mode 100644 plugins/agent-workbench/lib-agent/filewatch/src/impl/PathUtils.kt delete mode 100644 plugins/agent-workbench/lib-agent/filewatch/src/impl/watchservice/AbstractWatchService.kt delete mode 100644 plugins/agent-workbench/lib-agent/filewatch/src/impl/watchservice/MacOSXListeningWatchService.kt delete mode 100644 plugins/agent-workbench/lib-agent/filewatch/src/impl/watchservice/jna/MacFSEventsApi.kt delete mode 100644 plugins/agent-workbench/lib-agent/filewatch/src/impl/watchservice/jna/MacKQueueApi.kt delete mode 100644 plugins/agent-workbench/lib-agent/filewatch/testSrc/AgentWorkbenchDirectoryWatcherTest.kt delete mode 100644 plugins/agent-workbench/lib-agent/filewatch/testSrc/impl/DirectoryWatcherImplTest.kt delete mode 100644 plugins/agent-workbench/lib-agent/filewatch/testSrc/impl/watchservice/MacOSXListeningWatchServiceTest.kt delete mode 100644 plugins/agent-workbench/lib-agent/json/BUILD.bazel delete mode 100644 plugins/agent-workbench/lib-agent/json/api-dump.txt delete mode 100644 plugins/agent-workbench/lib-agent/json/exposed-third-party-api.txt delete mode 100644 plugins/agent-workbench/lib-agent/json/intellij.platform.ai.agent.json.iml delete mode 100644 plugins/agent-workbench/lib-agent/json/intellij.platform.ai.agent.json.tests.iml delete mode 100644 plugins/agent-workbench/lib-agent/json/resources/intellij.platform.ai.agent.json.xml delete mode 100644 plugins/agent-workbench/lib-agent/json/src/WorkbenchJsonParsingUtils.kt delete mode 100644 plugins/agent-workbench/lib-agent/json/src/WorkbenchJsonlScanner.kt delete mode 100644 plugins/agent-workbench/lib-agent/json/src/filebacked/FileBackedSessionChangeSet.kt delete mode 100644 plugins/agent-workbench/lib-agent/json/src/filebacked/FileBackedSessionEventClassifier.kt delete mode 100644 plugins/agent-workbench/lib-agent/json/src/filebacked/FileBackedSessionInvalidationState.kt delete mode 100644 plugins/agent-workbench/lib-agent/json/src/filebacked/FileBackedSessionUpdatesFlow.kt delete mode 100644 plugins/agent-workbench/lib-agent/json/src/filebacked/FileBackedSessionWatcher.kt delete mode 100644 plugins/agent-workbench/lib-agent/json/testSrc/filebacked/FileBackedSessionInvalidationStateTest.kt delete mode 100644 plugins/agent-workbench/lib-agent/json/testSrc/filebacked/FileBackedSessionUpdatesFlowTest.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/BUILD.bazel delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/api-dump.txt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/exposed-third-party-api.txt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/intellij.platform.ai.agent.sessions.core.iml delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/resources/intellij.platform.ai.agent.sessions.core.xml delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/AgentSessionThreadIds.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/AgentSessionThreadPresentation.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/AgentSessionThreadPresentationModel.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/AgentSessionThreadRebindPolicy.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/SessionActionTarget.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/config/AgentWorkbenchProjectRuntimeConfigProvider.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/cost/AgentSessionUsageSnapshot.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionLaunchContributor.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionLaunchPlanner.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionLaunchSpecAugmenter.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionLaunchSpecs.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionOutOfBandLaunch.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionSurfaces.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionThreadViewOpenPlan.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/launch/AwbMcpConfigBuilder.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/launch/AwbMcpConfigContributor.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/launch/AwbMcpConfigProviderContributor.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/launch/McpStreamUrlProvider.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/paths/AgentWorkbenchProjectPaths.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentOpenTopLevelThreadDispatchService.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentPromptProviderOption.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionLaunchProfiles.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionProviderDescriptor.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionProviderFeatureSettings.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionProviderMenuModel.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionProviderPlanMode.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionProviderUiContributor.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionProviders.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionSource.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/providers/BaseAgentSessionSource.kt delete mode 100644 plugins/agent-workbench/lib-agent/sessions-core/src/util/JbCentralCliSupport.kt delete mode 100644 plugins/agent-workbench/prompt/core/BUILD.bazel delete mode 100644 plugins/agent-workbench/prompt/core/intellij.agent.workbench.prompt.core.iml delete mode 100644 plugins/agent-workbench/prompt/core/intellij.agent.workbench.prompt.core.tests.iml delete mode 100644 plugins/agent-workbench/prompt/core/resources/intellij.agent.workbench.prompt.core.xml delete mode 100644 plugins/agent-workbench/prompt/core/resources/messages/AgentPromptSuggestionsBundle.properties delete mode 100644 plugins/agent-workbench/prompt/core/src/AgentPromptBuiltinContextRenderers.kt delete mode 100644 plugins/agent-workbench/prompt/core/src/AgentPromptContainerLauncher.kt delete mode 100644 plugins/agent-workbench/prompt/core/src/AgentPromptContextContributorBridge.kt delete mode 100644 plugins/agent-workbench/prompt/core/src/AgentPromptContextEnvelopeFormatter.kt delete mode 100644 plugins/agent-workbench/prompt/core/src/AgentPromptContextPayloads.kt delete mode 100644 plugins/agent-workbench/prompt/core/src/AgentPromptContextRendererBridge.kt delete mode 100644 plugins/agent-workbench/prompt/core/src/AgentPromptContextResolverService.kt delete mode 100644 plugins/agent-workbench/prompt/core/src/AgentPromptInvocationDataAttributes.kt delete mode 100644 plugins/agent-workbench/prompt/core/src/AgentPromptLauncherBridge.kt delete mode 100644 plugins/agent-workbench/prompt/core/src/AgentPromptManualContextSourceBridge.kt delete mode 100644 plugins/agent-workbench/prompt/core/src/AgentPromptModels.kt delete mode 100644 plugins/agent-workbench/prompt/core/src/AgentPromptPaletteExtension.kt delete mode 100644 plugins/agent-workbench/prompt/core/src/AgentPromptProjectPathContext.kt delete mode 100644 plugins/agent-workbench/prompt/core/src/AgentPromptReusableSourceEntry.kt delete mode 100644 plugins/agent-workbench/prompt/core/src/AgentPromptSuggestionBundle.kt delete mode 100644 plugins/agent-workbench/prompt/core/src/AgentPromptSuggestionGenerator.kt delete mode 100644 plugins/agent-workbench/prompt/core/src/AgentPromptSuggestionSeeds.kt delete mode 100644 plugins/agent-workbench/prompt/core/src/AgentPromptSuggestionSessionService.kt delete mode 100644 plugins/agent-workbench/prompt/core/testSrc/AgentPromptContextEnvelopeFormatterTest.kt delete mode 100644 plugins/agent-workbench/prompt/core/testSrc/AgentPromptContextResolverServiceTest.kt delete mode 100644 plugins/agent-workbench/prompt/core/testSrc/AgentPromptDefaultSuggestionGeneratorTest.kt delete mode 100644 plugins/agent-workbench/prompt/core/testSrc/AgentPromptSuggestionGeneratorTest.kt delete mode 100644 plugins/agent-workbench/prompt/core/testSrc/AgentPromptSuggestionSessionServiceTest.kt diff --git a/.idea/modules.xml b/.idea/modules.xml index 0b9e4fa1a489..a73dfb43345a 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -47,8 +47,6 @@ - - @@ -1234,17 +1232,6 @@ - - - - - - - - - - - diff --git a/BUILD.bazel b/BUILD.bazel index f78b851b4e41..3d8f82839a16 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -876,18 +876,6 @@ jvm_library( "//plugins/lombok:lombok-tests_test_lib", "//platform/util/concurrency:concurrency-tests", "//platform/util/concurrency:concurrency-tests_test_lib", - "//plugins/agent-workbench/lib-agent/cli:ai-agent-cli-tests", - "//plugins/agent-workbench/lib-agent/cli:ai-agent-cli-tests_test_lib", - "//plugins/agent-workbench/lib-agent/common:ai-agent-common-tests", - "//plugins/agent-workbench/lib-agent/common:ai-agent-common-tests_test_lib", - "//plugins/agent-workbench/lib-agent/core:ai-agent-core-tests", - "//plugins/agent-workbench/lib-agent/core:ai-agent-core-tests_test_lib", - "//plugins/agent-workbench/lib-agent/filewatch:ai-agent-filewatch-tests", - "//plugins/agent-workbench/lib-agent/filewatch:ai-agent-filewatch-tests_test_lib", - "//plugins/agent-workbench/lib-agent/json:ai-agent-json-tests", - "//plugins/agent-workbench/lib-agent/json:ai-agent-json-tests_test_lib", - "//plugins/agent-workbench/prompt/core:agent-workbench-prompt-core-tests", - "//plugins/agent-workbench/prompt/core:agent-workbench-prompt-core-tests_test_lib", "//plugins/ant:ant-tests", "//plugins/ant:ant-tests_test_lib", "//plugins/commander:commander-tests", diff --git a/build/bazel-generated-file-list.txt b/build/bazel-generated-file-list.txt index 8db5548f4850..3f69d602d0c4 100644 --- a/build/bazel-generated-file-list.txt +++ b/build/bazel-generated-file-list.txt @@ -979,13 +979,6 @@ plugins/ByteCodeViewer plugins/IntelliLang plugins/IntelliLang/backend plugins/IntelliLang/tests -plugins/agent-workbench/lib-agent/cli -plugins/agent-workbench/lib-agent/common -plugins/agent-workbench/lib-agent/core -plugins/agent-workbench/lib-agent/filewatch -plugins/agent-workbench/lib-agent/json -plugins/agent-workbench/lib-agent/sessions-core -plugins/agent-workbench/prompt/core plugins/ant plugins/ant/jps-plugin plugins/built-in-help diff --git a/intellij.idea.community.main.tests.iml b/intellij.idea.community.main.tests.iml index 65beec42f43a..3702ea85f94f 100644 --- a/intellij.idea.community.main.tests.iml +++ b/intellij.idea.community.main.tests.iml @@ -138,12 +138,6 @@ - - - - - - diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/impl/TestingTasksImpl.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/impl/TestingTasksImpl.kt index 1089e25a522a..74df2a4ec490 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/impl/TestingTasksImpl.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/impl/TestingTasksImpl.kt @@ -1489,12 +1489,6 @@ private suspend fun publishTestDiscovery(messages: BuildMessages, file: String?) private val COMMUNITY_AGGREGATOR_BAZEL_MIGRATED_MODULES = listOf( "intellij.maven.server.eventListener.tests", "intellij.agent.workbench.chat.tests", - "intellij.platform.ai.agent.cli.tests", - "intellij.platform.ai.agent.common.tests", - "intellij.platform.ai.agent.core.tests", - "intellij.platform.ai.agent.filewatch.tests", - "intellij.platform.ai.agent.json.tests", - "intellij.agent.workbench.prompt.core.tests", "intellij.ant.tests", "intellij.commander.tests", "intellij.completionMlRanking.tests", diff --git a/plugins/agent-workbench/lib-agent/cli/BUILD.bazel b/plugins/agent-workbench/lib-agent/cli/BUILD.bazel deleted file mode 100644 index d1e2f9cb691b..000000000000 --- a/plugins/agent-workbench/lib-agent/cli/BUILD.bazel +++ /dev/null @@ -1,133 +0,0 @@ -load("@community//build:tests-options.bzl", "jps_test") -load("@rules_jvm//:jvm.bzl", "jvm_library") - -### auto-generated section `build intellij.platform.ai.agent.cli` start - -jvm_library( - name = "cli", - srcs = glob( - [ - "src/**/*.kt", - "src/**/*.java", - "src/**/*.form", - ], - allow_empty = True, - ), - module_name = "intellij.platform.ai.agent.cli", - resource_strip_prefix = "resources", - resources = glob( - ["resources/**/*"], - ), - visibility = ["//visibility:public"], - deps = [ - # do not sort, - "@lib//:kotlin-stdlib", - "//platform/core-api:core", - "//platform/util", - "//platform/platform-util-io:ide-util-io", - "//platform/eel", - "//platform/eel-provider", - "//plugins/terminal", - ], -) - -jvm_library( - name = "cli_test_lib", - testonly = True, - srcs = glob( - [], - allow_empty = True, - ), - module_name = "intellij.platform.ai.agent.cli", - visibility = ["//visibility:public"], - runtime_deps = [ - # do not sort, - ":cli", - "//platform/core-api:core_test_lib", - "//platform/util:util_test_lib", - "//platform/platform-util-io:ide-util-io_test_lib", - "//platform/eel:eel_test_lib", - "//platform/eel-provider:eel-provider_test_lib", - "//plugins/terminal:terminal_test_lib", - ], -) -### auto-generated section `build intellij.platform.ai.agent.cli` end - -### auto-generated section `iml intellij.platform.ai.agent.cli` start -exports_files( - [ - "intellij.platform.ai.agent.cli.iml", - ], - visibility = ["//visibility:public"], -) -### auto-generated section `iml intellij.platform.ai.agent.cli` end - -### auto-generated section `build intellij.platform.ai.agent.cli.tests` start - -jvm_library( - name = "ai-agent-cli-tests", - srcs = glob( - [], - allow_empty = True, - ), - module_name = "intellij.platform.ai.agent.cli.tests", - visibility = ["//visibility:public"], -) - -jvm_library( - name = "ai-agent-cli-tests_test_lib", - testonly = True, - srcs = glob( - [ - "testSrc/**/*.kt", - "testSrc/**/*.java", - "testSrc/**/*.form", - ], - allow_empty = True, - ), - associates = [ - "//plugins/agent-workbench/lib-agent/cli", - "//plugins/agent-workbench/lib-agent/cli:cli_test_lib", - ], - visibility = ["//visibility:public"], - runtime_deps = [ - # do not sort, - ":ai-agent-cli-tests", - "//platform/platform-util-io:ide-util-io_test_lib", - ], - deps = [ - # do not sort, - "@lib//:kotlin-stdlib", - "//libraries/kotlinx/coroutines/core", - "//libraries/kotlinx/coroutines/core:core_test_lib", - "//platform/core-api:core", - "//platform/core-api:core_test_lib", - "//platform/util", - "//platform/util:util_test_lib", - "//platform/platform-util-io:ide-util-io", - "//plugins/terminal", - "//plugins/terminal:terminal_test_lib", - "//libraries/junit5", - "//libraries/junit5:junit5_test_lib", - "//libraries/assertj-core", - "//libraries/assertj-core:assertj-core_test_lib", - ], -) -### auto-generated section `build intellij.platform.ai.agent.cli.tests` end - -### auto-generated section `iml intellij.platform.ai.agent.cli.tests` start -exports_files( - [ - "intellij.platform.ai.agent.cli.tests.iml", - ], - visibility = ["//visibility:public"], -) -### auto-generated section `iml intellij.platform.ai.agent.cli.tests` end - -### auto-generated section `test intellij.platform.ai.agent.cli.tests` start - -jps_test( - name = "ai-agent-cli-tests_test", - runtime_deps = [":ai-agent-cli-tests_test_lib"], -) -### auto-generated section `test intellij.platform.ai.agent.cli.tests` end diff --git a/plugins/agent-workbench/lib-agent/cli/api-dump.txt b/plugins/agent-workbench/lib-agent/cli/api-dump.txt deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/plugins/agent-workbench/lib-agent/cli/intellij.platform.ai.agent.cli.iml b/plugins/agent-workbench/lib-agent/cli/intellij.platform.ai.agent.cli.iml deleted file mode 100644 index 9ca02a22f06f..000000000000 --- a/plugins/agent-workbench/lib-agent/cli/intellij.platform.ai.agent.cli.iml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/plugins/agent-workbench/lib-agent/cli/intellij.platform.ai.agent.cli.tests.iml b/plugins/agent-workbench/lib-agent/cli/intellij.platform.ai.agent.cli.tests.iml deleted file mode 100644 index e82df67b6861..000000000000 --- a/plugins/agent-workbench/lib-agent/cli/intellij.platform.ai.agent.cli.tests.iml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/plugins/agent-workbench/lib-agent/cli/resources/intellij.platform.ai.agent.cli.xml b/plugins/agent-workbench/lib-agent/cli/resources/intellij.platform.ai.agent.cli.xml deleted file mode 100644 index 54fcb179df89..000000000000 --- a/plugins/agent-workbench/lib-agent/cli/resources/intellij.platform.ai.agent.cli.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/plugins/agent-workbench/lib-agent/cli/src/AgentCliExecutableResolver.kt b/plugins/agent-workbench/lib-agent/cli/src/AgentCliExecutableResolver.kt deleted file mode 100644 index b5c6fb69977c..000000000000 --- a/plugins/agent-workbench/lib-agent/cli/src/AgentCliExecutableResolver.kt +++ /dev/null @@ -1,128 +0,0 @@ -// 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.cli - -import com.intellij.execution.configurations.PathEnvironmentVariableUtil -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.progress.ProcessCanceledException -import com.intellij.platform.eel.provider.LocalEelDescriptor -import com.intellij.platform.eel.provider.toEelApi -import kotlinx.coroutines.CancellationException -import org.jetbrains.annotations.ApiStatus -import org.jetbrains.plugins.terminal.agent.TerminalAgent -import org.jetbrains.plugins.terminal.agent.TerminalAgentResolver -import java.nio.file.Files -import java.nio.file.Path - -private val LOG = logger() - -private class AgentCliExecutableResolverLogCategory - -@ApiStatus.Internal -suspend fun resolveExecutableViaTerminalResolver( - terminalAgent: TerminalAgent, - binaryPathResolver: suspend (TerminalAgent) -> String? = ::resolveTerminalAgentBinaryPath, -): String? { - return try { - binaryPathResolver(terminalAgent) - } - catch (e: CancellationException) { - throw e - } - catch (e: ProcessCanceledException) { - throw e - } - catch (e: Throwable) { - LOG.debug("Failed to resolve binary for terminal agent '${terminalAgent.agentKey.key}'", e) - null - } -} - -@ApiStatus.Internal -suspend fun resolveExecutableViaTerminalResolver( - terminalAgentKey: String, - fallbackResolver: (() -> String?)? = null, - missingAgentMessage: String? = null, - terminalAgentLookup: (TerminalAgent.AgentKey) -> TerminalAgent? = TerminalAgent::findByKey, - binaryPathResolver: suspend (TerminalAgent) -> String? = ::resolveTerminalAgentBinaryPath, -): String? { - val terminalAgent = lookupTerminalAgent(terminalAgentKey, terminalAgentLookup) - if (terminalAgent == null) { - if (missingAgentMessage != null) { - LOG.warn(missingAgentMessage) - } - return fallbackResolver?.invoke() - } - return resolveExecutableViaTerminalResolver(terminalAgent, binaryPathResolver) ?: fallbackResolver?.invoke() -} - -@ApiStatus.Internal -suspend fun resolveExecutableOrDefaultViaTerminalResolver( - defaultCommand: String, - terminalAgent: TerminalAgent, - binaryPathResolver: suspend (TerminalAgent) -> String? = ::resolveTerminalAgentBinaryPath, -): String { - return resolveExecutableViaTerminalResolver(terminalAgent, binaryPathResolver) ?: defaultCommand -} - -@ApiStatus.Internal -suspend fun resolveExecutableOrDefaultViaTerminalResolver( - defaultCommand: String, - terminalAgentKey: String, - fallbackResolver: (() -> String?)? = null, - missingAgentMessage: String? = null, - terminalAgentLookup: (TerminalAgent.AgentKey) -> TerminalAgent? = TerminalAgent::findByKey, - binaryPathResolver: suspend (TerminalAgent) -> String? = ::resolveTerminalAgentBinaryPath, -): String { - return resolveExecutableViaTerminalResolver( - terminalAgentKey = terminalAgentKey, - fallbackResolver = fallbackResolver, - missingAgentMessage = missingAgentMessage, - terminalAgentLookup = terminalAgentLookup, - binaryPathResolver = binaryPathResolver, - ) ?: defaultCommand -} - -@ApiStatus.Internal -fun resolveExecutableFromPath(command: String): String? { - return PathEnvironmentVariableUtil.findExecutableInPathOnAnyOS(command)?.toPath()?.toAbsolutePath()?.toString() -} - -@ApiStatus.Internal -fun resolveExecutableFromPathOrLocalBin( - command: String, - localExecutableNames: List = listOf(command), - userHomePath: Path? = System.getProperty("user.home")?.let(Path::of), -): String? { - resolveExecutableFromPath(command)?.let { return it } - val homePath = userHomePath ?: return null - for (executableName in localExecutableNames) { - val localBin = homePath.resolve(".local").resolve("bin").resolve(executableName) - if (Files.isRegularFile(localBin)) return localBin.toAbsolutePath().toString() - } - return null -} - -@ApiStatus.Internal -fun terminalAgentBinaryNameOrDefault( - terminalAgentKey: String, - defaultCommand: String, - terminalAgentLookup: (TerminalAgent.AgentKey) -> TerminalAgent? = TerminalAgent::findByKey, -): String { - return lookupTerminalAgent(terminalAgentKey, terminalAgentLookup)?.binaryName ?: defaultCommand -} - -private fun lookupTerminalAgent( - terminalAgentKey: String, - terminalAgentLookup: (TerminalAgent.AgentKey) -> TerminalAgent?, -): TerminalAgent? { - return runCatching { - terminalAgentLookup(TerminalAgent.AgentKey(terminalAgentKey)) - }.onFailure { error -> - LOG.debug("Failed to resolve terminal agent for key '$terminalAgentKey'", error) - }.getOrNull() -} - -private suspend fun resolveTerminalAgentBinaryPath(terminalAgent: TerminalAgent): String? { - val eelApi = LocalEelDescriptor.toEelApi() - return TerminalAgentResolver.findBinaryPath(terminalAgent, eelApi) -} diff --git a/plugins/agent-workbench/lib-agent/cli/testSrc/AgentCliExecutableResolverTest.kt b/plugins/agent-workbench/lib-agent/cli/testSrc/AgentCliExecutableResolverTest.kt deleted file mode 100644 index 24524b9ada69..000000000000 --- a/plugins/agent-workbench/lib-agent/cli/testSrc/AgentCliExecutableResolverTest.kt +++ /dev/null @@ -1,210 +0,0 @@ -// 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.cli - -import com.intellij.openapi.progress.ProcessCanceledException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.runBlocking -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.assertThatThrownBy -import org.jetbrains.plugins.terminal.agent.TerminalAgent -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.io.TempDir -import java.nio.file.Files -import java.nio.file.Path -import javax.swing.Icon - -class AgentCliExecutableResolverTest { - @Test - fun directAgentResolutionUsesInjectedBinaryResolver(): Unit = runBlocking(Dispatchers.Default) { - val agent = testTerminalAgent(key = "codex", binaryName = "codex") - - val executable = resolveExecutableViaTerminalResolver(agent) { terminalAgent -> - "/opt/bin/${terminalAgent.binaryName}" - } - - assertThat(executable).isEqualTo("/opt/bin/codex") - } - - @Test - fun keyBasedResolutionUsesRegisteredAgent(): Unit = runBlocking(Dispatchers.Default) { - val agent = testTerminalAgent(key = "claude_code", binaryName = "claude") - - val executable = resolveExecutableViaTerminalResolver( - terminalAgentKey = "claude_code", - terminalAgentLookup = { key -> if (key.key == "claude_code") agent else null }, - binaryPathResolver = { terminalAgent -> "/usr/local/bin/${terminalAgent.binaryName}" }, - ) - - assertThat(executable).isEqualTo("/usr/local/bin/claude") - } - - @Test - fun registeredAgentUsesFallbackWhenBinaryLookupFindsNoExecutable(): Unit = runBlocking(Dispatchers.Default) { - val agent = testTerminalAgent(key = "codex", binaryName = "codex") - - val executable = resolveExecutableViaTerminalResolver( - terminalAgentKey = "codex", - fallbackResolver = { "/fallback/codex" }, - terminalAgentLookup = { key -> if (key.key == "codex") agent else null }, - binaryPathResolver = { null }, - ) - - assertThat(executable).isEqualTo("/fallback/codex") - } - - @Test - fun registeredAgentUsesFallbackWhenBinaryLookupFails(): Unit = runBlocking(Dispatchers.Default) { - val agent = testTerminalAgent(key = "codex", binaryName = "codex") - - val executable = resolveExecutableViaTerminalResolver( - terminalAgentKey = "codex", - fallbackResolver = { "/fallback/codex" }, - terminalAgentLookup = { key -> if (key.key == "codex") agent else null }, - binaryPathResolver = { error("broken binary lookup") }, - ) - - assertThat(executable).isEqualTo("/fallback/codex") - } - - @Test - fun binaryLookupCancellationPropagates() { - val agent = testTerminalAgent(key = "codex", binaryName = "codex") - - assertThatThrownBy { - runBlocking(Dispatchers.Default) { - resolveExecutableViaTerminalResolver(agent) { - throw CancellationException("cancelled") - } - } - }.isInstanceOf(CancellationException::class.java) - } - - @Test - fun binaryLookupProcessCancellationPropagates() { - val agent = testTerminalAgent(key = "codex", binaryName = "codex") - - assertThatThrownBy { - runBlocking(Dispatchers.Default) { - resolveExecutableViaTerminalResolver(agent) { - throw ProcessCanceledException() - } - } - }.isInstanceOf(ProcessCanceledException::class.java) - } - - @Test - fun missingAgentUsesFallbackWithoutBinaryLookup(): Unit = runBlocking(Dispatchers.Default) { - var binaryLookupCalled = false - - val executable = resolveExecutableViaTerminalResolver( - terminalAgentKey = "missing", - fallbackResolver = { "/fallback/agent" }, - terminalAgentLookup = { null }, - binaryPathResolver = { - binaryLookupCalled = true - "/unexpected" - }, - ) - - assertThat(executable).isEqualTo("/fallback/agent") - assertThat(binaryLookupCalled).isFalse() - } - - @Test - fun lookupFailureUsesFallbackWithoutBinaryLookup(): Unit = runBlocking(Dispatchers.Default) { - var binaryLookupCalled = false - - val executable = resolveExecutableViaTerminalResolver( - terminalAgentKey = "broken", - fallbackResolver = { "/fallback/agent" }, - terminalAgentLookup = { error("broken terminal agent extension") }, - binaryPathResolver = { - binaryLookupCalled = true - "/unexpected" - }, - ) - - assertThat(executable).isEqualTo("/fallback/agent") - assertThat(binaryLookupCalled).isFalse() - } - - @Test - fun defaultCommandIsReturnedWhenResolverFindsNoExecutable(): Unit = runBlocking(Dispatchers.Default) { - val executable = resolveExecutableOrDefaultViaTerminalResolver( - defaultCommand = "pi", - terminalAgent = testTerminalAgent(key = "pi", binaryName = "pi"), - binaryPathResolver = { null }, - ) - - assertThat(executable).isEqualTo("pi") - } - - @Test - fun terminalAgentBinaryNameFallsBackToDefault() { - val registered = testTerminalAgent(key = "codex", binaryName = "codex-nightly") - - assertThat( - terminalAgentBinaryNameOrDefault( - terminalAgentKey = "codex", - defaultCommand = "codex", - terminalAgentLookup = { key -> if (key.key == "codex") registered else null }, - ) - ).isEqualTo("codex-nightly") - assertThat( - terminalAgentBinaryNameOrDefault( - terminalAgentKey = "missing", - defaultCommand = "codex", - terminalAgentLookup = { null }, - ) - ).isEqualTo("codex") - assertThat( - terminalAgentBinaryNameOrDefault( - terminalAgentKey = "broken", - defaultCommand = "codex", - terminalAgentLookup = { error("broken terminal agent extension") }, - ) - ).isEqualTo("codex") - } - - @Test - fun localBinFallbackResolvesHomeRelativeExecutable(@TempDir homePath: Path) { - val executable = homePath.resolve(".local").resolve("bin").resolve("agent-test-cli") - Files.createDirectories(executable.parent) - Files.writeString(executable, "") - - assertThat( - resolveExecutableFromPathOrLocalBin( - command = "agent-test-cli-command-that-should-not-exist", - localExecutableNames = listOf("agent-test-cli"), - userHomePath = homePath, - ) - ).isEqualTo(executable.toAbsolutePath().toString()) - } - - @Test - fun localBinFallbackIgnoresNonFileCandidates(@TempDir homePath: Path) { - val executableDirectory = homePath.resolve(".local").resolve("bin").resolve("agent-test-cli") - Files.createDirectories(executableDirectory) - - assertThat( - resolveExecutableFromPathOrLocalBin( - command = "agent-test-cli-command-that-should-not-exist", - localExecutableNames = listOf("agent-test-cli"), - userHomePath = homePath, - ) - ).isNull() - } -} - -private fun testTerminalAgent( - key: String, - binaryName: String, -): TerminalAgent { - return object : TerminalAgent { - override val agentKey: TerminalAgent.AgentKey = TerminalAgent.AgentKey(key) - override val displayName: String = "Test Agent" - override val binaryName: String = binaryName - override val icon: Icon? = null - } -} diff --git a/plugins/agent-workbench/lib-agent/common/BUILD.bazel b/plugins/agent-workbench/lib-agent/common/BUILD.bazel deleted file mode 100644 index 8bf2f1b40663..000000000000 --- a/plugins/agent-workbench/lib-agent/common/BUILD.bazel +++ /dev/null @@ -1,128 +0,0 @@ -load("@community//build:tests-options.bzl", "jps_test") -load("@rules_jvm//:jvm.bzl", "jvm_library") - -### auto-generated section `build intellij.platform.ai.agent.common` start - -jvm_library( - name = "common", - srcs = glob( - [ - "src/**/*.kt", - "src/**/*.java", - "src/**/*.form", - ], - allow_empty = True, - ), - module_name = "intellij.platform.ai.agent.common", - resource_strip_prefix = "resources", - resources = glob( - ["resources/**/*"], - ), - visibility = ["//visibility:public"], - deps = [ - # do not sort, - "@lib//:kotlin-stdlib", - "//platform/util/concurrency", - "//platform/core-api:core", - "//platform/util", - "//plugins/agent-workbench/lib-agent/core", - "//platform/util:util-ui", - ], -) - -jvm_library( - name = "common_test_lib", - testonly = True, - srcs = glob( - [], - allow_empty = True, - ), - module_name = "intellij.platform.ai.agent.common", - visibility = ["//visibility:public"], - runtime_deps = [ - # do not sort, - ":common", - "//platform/util/concurrency:concurrency_test_lib", - "//platform/core-api:core_test_lib", - "//platform/util:util_test_lib", - "//plugins/agent-workbench/lib-agent/core:core_test_lib", - "//platform/util:util-ui_test_lib", - "//libraries/junit5", - "//libraries/junit5:junit5_test_lib", - "//libraries/assertj-core", - "//libraries/assertj-core:assertj-core_test_lib", - ], -) -### auto-generated section `build intellij.platform.ai.agent.common` end - -### auto-generated section `iml intellij.platform.ai.agent.common` start -exports_files( - [ - "intellij.platform.ai.agent.common.iml", - ], - visibility = ["//visibility:public"], -) -### auto-generated section `iml intellij.platform.ai.agent.common` end - -### auto-generated section `build intellij.platform.ai.agent.common.tests` start - -jvm_library( - name = "ai-agent-common-tests", - srcs = glob( - [], - allow_empty = True, - ), - module_name = "intellij.platform.ai.agent.common.tests", - visibility = ["//visibility:public"], -) - -jvm_library( - name = "ai-agent-common-tests_test_lib", - testonly = True, - srcs = glob( - [ - "testSrc/**/*.kt", - "testSrc/**/*.java", - "testSrc/**/*.form", - ], - allow_empty = True, - ), - associates = [ - "//plugins/agent-workbench/lib-agent/common", - "//plugins/agent-workbench/lib-agent/common:common_test_lib", - ], - visibility = ["//visibility:public"], - runtime_deps = [":ai-agent-common-tests"], - deps = [ - # do not sort, - "@lib//:kotlin-stdlib", - "//platform/util", - "//platform/util:util_test_lib", - "//platform/util:util-ui", - "//platform/util:util-ui_test_lib", - "//plugins/agent-workbench/lib-agent/core", - "//plugins/agent-workbench/lib-agent/core:core_test_lib", - "//libraries/junit5", - "//libraries/junit5:junit5_test_lib", - "//libraries/assertj-core", - "//libraries/assertj-core:assertj-core_test_lib", - ], -) -### auto-generated section `build intellij.platform.ai.agent.common.tests` end - -### auto-generated section `iml intellij.platform.ai.agent.common.tests` start -exports_files( - [ - "intellij.platform.ai.agent.common.tests.iml", - ], - visibility = ["//visibility:public"], -) -### auto-generated section `iml intellij.platform.ai.agent.common.tests` end - -### auto-generated section `test intellij.platform.ai.agent.common.tests` start - -jps_test( - name = "ai-agent-common-tests_test", - runtime_deps = [":ai-agent-common-tests_test_lib"], -) -### auto-generated section `test intellij.platform.ai.agent.common.tests` end diff --git a/plugins/agent-workbench/lib-agent/common/api-dump.txt b/plugins/agent-workbench/lib-agent/common/api-dump.txt deleted file mode 100644 index d47430a969ac..000000000000 --- a/plugins/agent-workbench/lib-agent/common/api-dump.txt +++ /dev/null @@ -1,36 +0,0 @@ -f:com.intellij.platform.ai.agent.common.AgentThreadActivityIconsKt -- sf:withAgentThreadActivityBadge(javax.swing.Icon,com.intellij.platform.ai.agent.core.AgentThreadActivity):javax.swing.Icon -f:com.intellij.platform.ai.agent.common.AgentThreadActivityPresentation -- f:darkFallbackRgb:java.lang.Integer -- f:lightFallbackRgb:java.lang.Integer -- f:namedColorKey:java.lang.String -- f:showBadge:Z -- f:statusMessageKey:java.lang.String -- (java.lang.String,java.lang.Integer,java.lang.Integer,java.lang.String,Z):V -- b:(java.lang.String,java.lang.Integer,java.lang.Integer,java.lang.String,Z,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- f:component1():java.lang.String -- f:component2():java.lang.Integer -- f:component3():java.lang.Integer -- f:component4():java.lang.String -- f:component5():Z -- f:copy(java.lang.String,java.lang.Integer,java.lang.Integer,java.lang.String,Z):com.intellij.platform.ai.agent.common.AgentThreadActivityPresentation -- bs:copy$default(com.intellij.platform.ai.agent.common.AgentThreadActivityPresentation,java.lang.String,java.lang.Integer,java.lang.Integer,java.lang.String,Z,I,java.lang.Object):com.intellij.platform.ai.agent.common.AgentThreadActivityPresentation -- equals(java.lang.Object):Z -- f:getNamedColor():com.intellij.ui.JBColor -- hashCode():I -f:com.intellij.platform.ai.agent.common.AgentThreadActivityPresentationKt -- sf:presentation(com.intellij.platform.ai.agent.core.AgentThreadActivity):com.intellij.platform.ai.agent.common.AgentThreadActivityPresentation -- sf:statusBadgeColor(com.intellij.platform.ai.agent.core.AgentThreadActivity):java.awt.Color -- sf:statusColor(com.intellij.platform.ai.agent.core.AgentThreadActivity):java.awt.Color -- sf:statusMessageKey(com.intellij.platform.ai.agent.core.AgentThreadActivity):java.lang.String -f:com.intellij.platform.ai.agent.common.icons.AgentWorkbenchCommonIcons -- sf:Claude:javax.swing.Icon -- sf:ClaudeGray:javax.swing.Icon -- sf:Codex:javax.swing.Icon -- sf:CodexGray:javax.swing.Icon -- sf:Junie:javax.swing.Icon -- sf:JunieGray:javax.swing.Icon -- sf:Opencode:javax.swing.Icon -- sf:OpencodeGray:javax.swing.Icon -- sf:PI:javax.swing.Icon -- ():V diff --git a/plugins/agent-workbench/lib-agent/common/exposed-third-party-api.txt b/plugins/agent-workbench/lib-agent/common/exposed-third-party-api.txt deleted file mode 100644 index 1ff3abdfef32..000000000000 --- a/plugins/agent-workbench/lib-agent/common/exposed-third-party-api.txt +++ /dev/null @@ -1 +0,0 @@ -kotlin/jvm/internal/DefaultConstructorMarker diff --git a/plugins/agent-workbench/lib-agent/common/intellij.platform.ai.agent.common.iml b/plugins/agent-workbench/lib-agent/common/intellij.platform.ai.agent.common.iml deleted file mode 100644 index 53abddcc8905..000000000000 --- a/plugins/agent-workbench/lib-agent/common/intellij.platform.ai.agent.common.iml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/plugins/agent-workbench/lib-agent/common/intellij.platform.ai.agent.common.tests.iml b/plugins/agent-workbench/lib-agent/common/intellij.platform.ai.agent.common.tests.iml deleted file mode 100644 index e45e71009bf3..000000000000 --- a/plugins/agent-workbench/lib-agent/common/intellij.platform.ai.agent.common.tests.iml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/plugins/agent-workbench/lib-agent/common/resources/icons/claude.svg b/plugins/agent-workbench/lib-agent/common/resources/icons/claude.svg deleted file mode 100644 index 608e410c88dc..000000000000 --- a/plugins/agent-workbench/lib-agent/common/resources/icons/claude.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/plugins/agent-workbench/lib-agent/common/resources/icons/claudeGray.svg b/plugins/agent-workbench/lib-agent/common/resources/icons/claudeGray.svg deleted file mode 100644 index 8c29b71a2d4f..000000000000 --- a/plugins/agent-workbench/lib-agent/common/resources/icons/claudeGray.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/plugins/agent-workbench/lib-agent/common/resources/icons/claudeGray_dark.svg b/plugins/agent-workbench/lib-agent/common/resources/icons/claudeGray_dark.svg deleted file mode 100644 index 7760a3972166..000000000000 --- a/plugins/agent-workbench/lib-agent/common/resources/icons/claudeGray_dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/plugins/agent-workbench/lib-agent/common/resources/icons/codex.svg b/plugins/agent-workbench/lib-agent/common/resources/icons/codex.svg deleted file mode 100644 index 260bd482b98f..000000000000 --- a/plugins/agent-workbench/lib-agent/common/resources/icons/codex.svg +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/plugins/agent-workbench/lib-agent/common/resources/icons/codexGray.svg b/plugins/agent-workbench/lib-agent/common/resources/icons/codexGray.svg deleted file mode 100644 index 01f9c93170b1..000000000000 --- a/plugins/agent-workbench/lib-agent/common/resources/icons/codexGray.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/plugins/agent-workbench/lib-agent/common/resources/icons/codexGray_dark.svg b/plugins/agent-workbench/lib-agent/common/resources/icons/codexGray_dark.svg deleted file mode 100644 index b7649320c888..000000000000 --- a/plugins/agent-workbench/lib-agent/common/resources/icons/codexGray_dark.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/plugins/agent-workbench/lib-agent/common/resources/icons/junie.svg b/plugins/agent-workbench/lib-agent/common/resources/icons/junie.svg deleted file mode 100644 index 50ff09c43518..000000000000 --- a/plugins/agent-workbench/lib-agent/common/resources/icons/junie.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/plugins/agent-workbench/lib-agent/common/resources/icons/junieGray.svg b/plugins/agent-workbench/lib-agent/common/resources/icons/junieGray.svg deleted file mode 100644 index 96df5d3d3e08..000000000000 --- a/plugins/agent-workbench/lib-agent/common/resources/icons/junieGray.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/plugins/agent-workbench/lib-agent/common/resources/icons/junieGray_dark.svg b/plugins/agent-workbench/lib-agent/common/resources/icons/junieGray_dark.svg deleted file mode 100644 index 94174d8d84ee..000000000000 --- a/plugins/agent-workbench/lib-agent/common/resources/icons/junieGray_dark.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/plugins/agent-workbench/lib-agent/common/resources/icons/opencode.svg b/plugins/agent-workbench/lib-agent/common/resources/icons/opencode.svg deleted file mode 100644 index b36d603ac649..000000000000 --- a/plugins/agent-workbench/lib-agent/common/resources/icons/opencode.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/plugins/agent-workbench/lib-agent/common/resources/icons/opencodeGray.svg b/plugins/agent-workbench/lib-agent/common/resources/icons/opencodeGray.svg deleted file mode 100644 index 2f2fc6527c08..000000000000 --- a/plugins/agent-workbench/lib-agent/common/resources/icons/opencodeGray.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/plugins/agent-workbench/lib-agent/common/resources/icons/opencodeGray_dark.svg b/plugins/agent-workbench/lib-agent/common/resources/icons/opencodeGray_dark.svg deleted file mode 100644 index d1c887a28504..000000000000 --- a/plugins/agent-workbench/lib-agent/common/resources/icons/opencodeGray_dark.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/plugins/agent-workbench/lib-agent/common/resources/icons/pi.svg b/plugins/agent-workbench/lib-agent/common/resources/icons/pi.svg deleted file mode 100644 index 5b44c8dae766..000000000000 --- a/plugins/agent-workbench/lib-agent/common/resources/icons/pi.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/plugins/agent-workbench/lib-agent/common/resources/icons/pi_dark.svg b/plugins/agent-workbench/lib-agent/common/resources/icons/pi_dark.svg deleted file mode 100644 index 9dea38a1d1bf..000000000000 --- a/plugins/agent-workbench/lib-agent/common/resources/icons/pi_dark.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/plugins/agent-workbench/lib-agent/common/resources/intellij.platform.ai.agent.common.xml b/plugins/agent-workbench/lib-agent/common/resources/intellij.platform.ai.agent.common.xml deleted file mode 100644 index 5c833a56b5f1..000000000000 --- a/plugins/agent-workbench/lib-agent/common/resources/intellij.platform.ai.agent.common.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/plugins/agent-workbench/lib-agent/common/src/AgentThreadActivityIcons.kt b/plugins/agent-workbench/lib-agent/common/src/AgentThreadActivityIcons.kt deleted file mode 100644 index a06ae84001c2..000000000000 --- a/plugins/agent-workbench/lib-agent/common/src/AgentThreadActivityIcons.kt +++ /dev/null @@ -1,11 +0,0 @@ -// 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.common - -import com.intellij.platform.ai.agent.core.AgentThreadActivity -import com.intellij.ui.IconManager -import javax.swing.Icon - -fun withAgentThreadActivityBadge(baseIcon: Icon, activity: AgentThreadActivity): Icon { - val badgeColor = activity.statusBadgeColor() ?: return baseIcon - return IconManager.getInstance().withIconBadge(baseIcon, badgeColor) -} diff --git a/plugins/agent-workbench/lib-agent/common/src/AgentThreadActivityPresentation.kt b/plugins/agent-workbench/lib-agent/common/src/AgentThreadActivityPresentation.kt deleted file mode 100644 index eb4c13ad18e5..000000000000 --- a/plugins/agent-workbench/lib-agent/common/src/AgentThreadActivityPresentation.kt +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -@file:Suppress("UseJBColor") - -package com.intellij.platform.ai.agent.common - -import com.intellij.platform.ai.agent.core.AgentThreadActivity -import com.intellij.ui.JBColor -import java.awt.Color - -data class AgentThreadActivityPresentation( - @JvmField val namedColorKey: String?, - @JvmField val lightFallbackRgb: Int?, - @JvmField val darkFallbackRgb: Int?, - @JvmField val statusMessageKey: String, - @JvmField val showBadge: Boolean = true, -) { - val namedColor: JBColor? = namedColorKey?.let { key -> - JBColor.namedColor(key, JBColor(requireNotNull(lightFallbackRgb), requireNotNull(darkFallbackRgb))) - } - - override fun toString(): String { - return "AgentThreadActivityPresentation(" + - "$namedColorKey, " + - "${lightFallbackRgb?.let { Color(it) }}, " + - "${darkFallbackRgb?.let { Color(it) }}, " + - "$statusMessageKey, " + - "$showBadge" + - ")" - } -} - -private fun threadActivityPresentation( - namedColorKey: String, - lightFallbackColor: Int, - darkFallbackColor: Int, - statusMessageKey: String, - showBadge: Boolean = true, -): AgentThreadActivityPresentation { - return AgentThreadActivityPresentation( - namedColorKey = namedColorKey, - lightFallbackRgb = lightFallbackColor, - darkFallbackRgb = darkFallbackColor, - statusMessageKey = statusMessageKey, - showBadge = showBadge, - ) -} - -// Idle/no-signal state: intentionally unaccented and unbadged. -private val READY_PRESENTATION = AgentThreadActivityPresentation( - namedColorKey = null, - lightFallbackRgb = null, - darkFallbackRgb = null, - statusMessageKey = "toolwindow.thread.status.ready", - showBadge = false, -) - -// Active work in progress. -private val PROCESSING_PRESENTATION = threadActivityPresentation( - namedColorKey = "IconBadge.successBackground", - lightFallbackColor = Color(0x55A76A).rgb, - darkFallbackColor = Color(0x5FAD65).rgb, - statusMessageKey = "toolwindow.thread.status.in.progress", -) - -// Review-mode output waiting for user attention. -private val REVIEWING_PRESENTATION = threadActivityPresentation( - namedColorKey = "IconBadge.warningBackground", - lightFallbackColor = Color(0xFFAF0F).rgb, - darkFallbackColor = Color(0xF2C55C).rgb, - statusMessageKey = "toolwindow.thread.status.needs.review", -) - -// Completed assistant output waiting to be seen. -private val UNREAD_PRESENTATION = threadActivityPresentation( - namedColorKey = "IconBadge.infoBackground", - lightFallbackColor = Color(0x588CF3).rgb, - darkFallbackColor = Color(0x548AF7).rgb, - statusMessageKey = "toolwindow.thread.status.done", -) - -// Explicit user action required. -private val NEEDS_INPUT_PRESENTATION = threadActivityPresentation( - namedColorKey = "IconBadge.warningBackground", - lightFallbackColor = Color(0xFFAF0F).rgb, - darkFallbackColor = Color(0xF2C55C).rgb, - statusMessageKey = "toolwindow.thread.status.needs.input", -) - -fun AgentThreadActivity.presentation(): AgentThreadActivityPresentation { - return when (this) { - AgentThreadActivity.READY -> READY_PRESENTATION - AgentThreadActivity.PROCESSING -> PROCESSING_PRESENTATION - AgentThreadActivity.REVIEWING -> REVIEWING_PRESENTATION - AgentThreadActivity.NEEDS_INPUT -> NEEDS_INPUT_PRESENTATION - AgentThreadActivity.UNREAD -> UNREAD_PRESENTATION - } -} - -fun AgentThreadActivity.statusColor(): Color? = presentation().namedColor - -fun AgentThreadActivity.statusBadgeColor(): Color? { - val presentation = presentation() - return presentation.namedColor?.takeIf { presentation.showBadge } -} - -fun AgentThreadActivity.statusMessageKey(): String = presentation().statusMessageKey diff --git a/plugins/agent-workbench/lib-agent/common/src/icons/AgentWorkbenchCommonIcons.java b/plugins/agent-workbench/lib-agent/common/src/icons/AgentWorkbenchCommonIcons.java deleted file mode 100644 index 13bdf9f5917f..000000000000 --- a/plugins/agent-workbench/lib-agent/common/src/icons/AgentWorkbenchCommonIcons.java +++ /dev/null @@ -1,26 +0,0 @@ -// 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.common.icons; - -import com.intellij.ui.IconManager; -import org.jetbrains.annotations.NotNull; - -import javax.swing.*; - -/** - * NOTE THIS FILE IS AUTO-GENERATED - * DO NOT EDIT IT BY HAND, run "Generate icon classes" configuration instead - */ -public final class AgentWorkbenchCommonIcons { - private static @NotNull Icon load(@NotNull String path, int cacheKey, int flags) { - return IconManager.getInstance().loadRasterizedIcon(path, AgentWorkbenchCommonIcons.class.getClassLoader(), cacheKey, flags); - } - /** 16x16 */ public static final @NotNull Icon Claude = load("icons/claude.svg", -294097785, 0); - /** 16x16 */ public static final @NotNull Icon ClaudeGray = load("icons/claudeGray.svg", 1540268029, 2); - /** 16x16 */ public static final @NotNull Icon Codex = load("icons/codex.svg", -72483386, 0); - /** 16x16 */ public static final @NotNull Icon CodexGray = load("icons/codexGray.svg", 1835398902, 2); - /** 16x16 */ public static final @NotNull Icon Junie = load("icons/junie.svg", -76396756, 0); - /** 16x16 */ public static final @NotNull Icon JunieGray = load("icons/junieGray.svg", 465929641, 2); - /** 16x16 */ public static final @NotNull Icon Opencode = load("icons/opencode.svg", -1522185687, 0); - /** 16x16 */ public static final @NotNull Icon OpencodeGray = load("icons/opencodeGray.svg", 1279569419, 2); - /** 16x16 */ public static final @NotNull Icon PI = load("icons/pi.svg", 1349425160, 2); -} diff --git a/plugins/agent-workbench/lib-agent/common/testSrc/AgentThreadActivityPresentationTest.kt b/plugins/agent-workbench/lib-agent/common/testSrc/AgentThreadActivityPresentationTest.kt deleted file mode 100644 index 10003b1e580c..000000000000 --- a/plugins/agent-workbench/lib-agent/common/testSrc/AgentThreadActivityPresentationTest.kt +++ /dev/null @@ -1,94 +0,0 @@ -// 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.common - -import com.intellij.platform.ai.agent.core.AgentThreadActivity -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.Timeout -import java.awt.Color -import java.util.concurrent.TimeUnit -import javax.swing.UIManager - -@Timeout(value = 2, unit = TimeUnit.MINUTES) -class AgentThreadActivityPresentationTest { - @Test - fun usesThreadPresentationsAndMessageKeys() { - val expected = linkedMapOf( - AgentThreadActivity.READY to AgentThreadActivityPresentation( - namedColorKey = null, - lightFallbackRgb = null, - darkFallbackRgb = null, - statusMessageKey = "toolwindow.thread.status.ready", - showBadge = false, - ), - AgentThreadActivity.PROCESSING to AgentThreadActivityPresentation( - namedColorKey = "IconBadge.successBackground", - lightFallbackRgb = Color(0x55A76A).rgb, - darkFallbackRgb = Color(0x5FAD65).rgb, - statusMessageKey = "toolwindow.thread.status.in.progress", - ), - AgentThreadActivity.REVIEWING to AgentThreadActivityPresentation( - namedColorKey = "IconBadge.warningBackground", - lightFallbackRgb = Color(0xFFAF0F).rgb, - darkFallbackRgb = Color(0xF2C55C).rgb, - statusMessageKey = "toolwindow.thread.status.needs.review", - ), - AgentThreadActivity.UNREAD to AgentThreadActivityPresentation( - namedColorKey = "IconBadge.infoBackground", - lightFallbackRgb = Color(0x588CF3).rgb, - darkFallbackRgb = Color(0x548AF7).rgb, - statusMessageKey = "toolwindow.thread.status.done", - ), - AgentThreadActivity.NEEDS_INPUT to AgentThreadActivityPresentation( - namedColorKey = "IconBadge.warningBackground", - lightFallbackRgb = Color(0xFFAF0F).rgb, - darkFallbackRgb = Color(0xF2C55C).rgb, - statusMessageKey = "toolwindow.thread.status.needs.input", - ), - ) - - expected.forEach { (activity, presentation) -> - assertThat(activity.presentation()).isEqualTo(presentation) - val statusColor = activity.statusColor() - if (presentation.namedColorKey == null) { - assertThat(statusColor).isNull() - } - else { - assertThat(statusColor?.rgb).isIn( - Color(requireNotNull(presentation.lightFallbackRgb)).rgb, - Color(requireNotNull(presentation.darkFallbackRgb)).rgb, - ) - } - assertThat(activity.statusBadgeColor()?.rgb).isEqualTo(if (presentation.showBadge) statusColor?.rgb else null) - assertThat(activity.statusMessageKey()).isEqualTo(presentation.statusMessageKey) - } - } - - @Test - fun prefersUiManagerOverrideForNamedColor() { - val overrideColor = Color(0x112233) - - withTemporaryNeedsInputUiColor(overrideColor) { - assertThat(AgentThreadActivity.NEEDS_INPUT.statusColor()?.rgb).isEqualTo(overrideColor.rgb) - assertThat(AgentThreadActivity.NEEDS_INPUT.statusBadgeColor()?.rgb).isEqualTo(overrideColor.rgb) - } - } - - private fun withTemporaryNeedsInputUiColor(color: Color, action: () -> Unit) { - val key = "IconBadge.warningBackground" - val defaults = UIManager.getDefaults() - val previous = defaults[key] - UIManager.put(key, color) - try { - action() - } - finally { - if (previous == null) { - defaults.remove(key) - } - else { - UIManager.put(key, previous) - } - } - } -} diff --git a/plugins/agent-workbench/lib-agent/core/BUILD.bazel b/plugins/agent-workbench/lib-agent/core/BUILD.bazel deleted file mode 100644 index 137a2a05c6ed..000000000000 --- a/plugins/agent-workbench/lib-agent/core/BUILD.bazel +++ /dev/null @@ -1,120 +0,0 @@ -load("@community//build:tests-options.bzl", "jps_test") -load("@rules_jvm//:jvm.bzl", "jvm_library") - -### auto-generated section `build intellij.platform.ai.agent.core` start - -jvm_library( - name = "core", - srcs = glob( - [ - "src/**/*.kt", - "src/**/*.java", - "src/**/*.form", - ], - allow_empty = True, - ), - module_name = "intellij.platform.ai.agent.core", - resource_strip_prefix = "resources", - resources = glob( - ["resources/**/*"], - ), - visibility = ["//visibility:public"], - deps = [ - # do not sort, - "@lib//:kotlin-stdlib", - "//platform/util/concurrency", - "//platform/core-api:core", - "//platform/util", - ], -) - -jvm_library( - name = "core_test_lib", - testonly = True, - srcs = glob( - [], - allow_empty = True, - ), - module_name = "intellij.platform.ai.agent.core", - visibility = ["//visibility:public"], - runtime_deps = [ - # do not sort, - ":core", - "//platform/util/concurrency:concurrency_test_lib", - "//platform/core-api:core_test_lib", - "//platform/util:util_test_lib", - ], -) -### auto-generated section `build intellij.platform.ai.agent.core` end - -### auto-generated section `iml intellij.platform.ai.agent.core` start -exports_files( - [ - "intellij.platform.ai.agent.core.iml", - ], - visibility = ["//visibility:public"], -) -### auto-generated section `iml intellij.platform.ai.agent.core` end - -### auto-generated section `build intellij.platform.ai.agent.core.tests` start - -jvm_library( - name = "ai-agent-core-tests", - srcs = glob( - [], - allow_empty = True, - ), - module_name = "intellij.platform.ai.agent.core.tests", - visibility = ["//visibility:public"], -) - -jvm_library( - name = "ai-agent-core-tests_test_lib", - testonly = True, - srcs = glob( - [ - "testSrc/**/*.kt", - "testSrc/**/*.java", - "testSrc/**/*.form", - ], - allow_empty = True, - ), - associates = [ - "//plugins/agent-workbench/lib-agent/core", - "//plugins/agent-workbench/lib-agent/core:core_test_lib", - ], - visibility = ["//visibility:public"], - runtime_deps = [":ai-agent-core-tests"], - deps = [ - # do not sort, - "@lib//:kotlin-stdlib", - "//platform/util/concurrency", - "//platform/util/concurrency:concurrency_test_lib", - "//platform/core-api:core", - "//platform/core-api:core_test_lib", - "//platform/util", - "//platform/util:util_test_lib", - "//libraries/junit5", - "//libraries/junit5:junit5_test_lib", - "//libraries/assertj-core", - "//libraries/assertj-core:assertj-core_test_lib", - ], -) -### auto-generated section `build intellij.platform.ai.agent.core.tests` end - -### auto-generated section `iml intellij.platform.ai.agent.core.tests` start -exports_files( - [ - "intellij.platform.ai.agent.core.tests.iml", - ], - visibility = ["//visibility:public"], -) -### auto-generated section `iml intellij.platform.ai.agent.core.tests` end - -### auto-generated section `test intellij.platform.ai.agent.core.tests` start - -jps_test( - name = "ai-agent-core-tests_test", - runtime_deps = [":ai-agent-core-tests_test_lib"], -) -### auto-generated section `test intellij.platform.ai.agent.core.tests` end diff --git a/plugins/agent-workbench/lib-agent/core/api-dump.txt b/plugins/agent-workbench/lib-agent/core/api-dump.txt deleted file mode 100644 index 06c27eb3a5c9..000000000000 --- a/plugins/agent-workbench/lib-agent/core/api-dump.txt +++ /dev/null @@ -1,256 +0,0 @@ -e:com.intellij.platform.ai.agent.core.AgentThreadActivity -- java.lang.Enum -- sf:NEEDS_INPUT:com.intellij.platform.ai.agent.core.AgentThreadActivity -- sf:PROCESSING:com.intellij.platform.ai.agent.core.AgentThreadActivity -- sf:READY:com.intellij.platform.ai.agent.core.AgentThreadActivity -- sf:REVIEWING:com.intellij.platform.ai.agent.core.AgentThreadActivity -- sf:UNREAD:com.intellij.platform.ai.agent.core.AgentThreadActivity -- s:getEntries():kotlin.enums.EnumEntries -- s:valueOf(java.lang.String):com.intellij.platform.ai.agent.core.AgentThreadActivity -- s:values():com.intellij.platform.ai.agent.core.AgentThreadActivity[] -e:com.intellij.platform.ai.agent.core.AgentThreadActivityBucket -- java.lang.Enum -- sf:ATTENTION:com.intellij.platform.ai.agent.core.AgentThreadActivityBucket -- sf:DONE:com.intellij.platform.ai.agent.core.AgentThreadActivityBucket -- sf:RUNNING:com.intellij.platform.ai.agent.core.AgentThreadActivityBucket -- s:getEntries():kotlin.enums.EnumEntries -- s:valueOf(java.lang.String):com.intellij.platform.ai.agent.core.AgentThreadActivityBucket -- s:values():com.intellij.platform.ai.agent.core.AgentThreadActivityBucket[] -f:com.intellij.platform.ai.agent.core.AgentThreadActivityKt -- sf:chromeBucket(com.intellij.platform.ai.agent.core.AgentThreadActivityReport):com.intellij.platform.ai.agent.core.AgentThreadActivityBucket -- sf:isWorking(com.intellij.platform.ai.agent.core.AgentThreadActivity):Z -f:com.intellij.platform.ai.agent.core.AgentThreadActivityReport -- sf:Companion:com.intellij.platform.ai.agent.core.AgentThreadActivityReport$Companion -- sf:READY:com.intellij.platform.ai.agent.core.AgentThreadActivityReport -- f:chromeActivity:com.intellij.platform.ai.agent.core.AgentThreadActivity -- f:rowActivity:com.intellij.platform.ai.agent.core.AgentThreadActivity -- (com.intellij.platform.ai.agent.core.AgentThreadActivity,com.intellij.platform.ai.agent.core.AgentThreadActivity):V -- b:(com.intellij.platform.ai.agent.core.AgentThreadActivity,com.intellij.platform.ai.agent.core.AgentThreadActivity,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- f:component1():com.intellij.platform.ai.agent.core.AgentThreadActivity -- f:component2():com.intellij.platform.ai.agent.core.AgentThreadActivity -- f:copy(com.intellij.platform.ai.agent.core.AgentThreadActivity,com.intellij.platform.ai.agent.core.AgentThreadActivity):com.intellij.platform.ai.agent.core.AgentThreadActivityReport -- bs:copy$default(com.intellij.platform.ai.agent.core.AgentThreadActivityReport,com.intellij.platform.ai.agent.core.AgentThreadActivity,com.intellij.platform.ai.agent.core.AgentThreadActivity,I,java.lang.Object):com.intellij.platform.ai.agent.core.AgentThreadActivityReport -- equals(java.lang.Object):Z -- hashCode():I -f:com.intellij.platform.ai.agent.core.AgentThreadActivityReport$Companion -f:com.intellij.platform.ai.agent.core.AgentThreadIdentity -- f:providerId:java.lang.String -- f:threadId:java.lang.String -- (java.lang.String,java.lang.String):V -- f:component1():java.lang.String -- f:component2():java.lang.String -- f:copy(java.lang.String,java.lang.String):com.intellij.platform.ai.agent.core.AgentThreadIdentity -- bs:copy$default(com.intellij.platform.ai.agent.core.AgentThreadIdentity,java.lang.String,java.lang.String,I,java.lang.Object):com.intellij.platform.ai.agent.core.AgentThreadIdentity -- equals(java.lang.Object):Z -- hashCode():I -f:com.intellij.platform.ai.agent.core.AgentThreadIdentityKt -- sf:buildAgentThreadIdentity(java.lang.String,java.lang.String):java.lang.String -- sf:parseAgentThreadIdentity(java.lang.String):com.intellij.platform.ai.agent.core.AgentThreadIdentity -f:com.intellij.platform.ai.agent.core.AgentWorkbenchPathUtilKt -- sf:normalizeAgentSessionProjectPath(java.lang.String):java.lang.String -- sf:normalizeAgentSessionTitle(java.lang.String):java.lang.String -- sf:normalizeAgentWorkbenchPath(java.lang.String):java.lang.String -- sf:normalizeAgentWorkbenchPathOrNull(java.lang.String):java.lang.String -- sf:parseAgentWorkbenchPathOrNull(java.lang.String):java.nio.file.Path -f:com.intellij.platform.ai.agent.core.extensions.OverridableValue -- (kotlin.jvm.functions.Function0):V -- f:value():java.lang.Object -- f:withOverride(java.lang.Object,kotlin.jvm.functions.Function0):java.lang.Object -f:com.intellij.platform.ai.agent.core.extensions.SingleExtensionPointResolver -- (com.intellij.openapi.diagnostic.Logger,com.intellij.openapi.extensions.ExtensionPointName,java.lang.String,kotlin.jvm.functions.Function1):V -- f:findFirstOrNull():java.lang.Object -f:com.intellij.platform.ai.agent.core.extensions.SnapshotExtensionPointCache -- (com.intellij.openapi.diagnostic.Logger,com.intellij.openapi.extensions.ExtensionPointName,java.lang.Class,java.lang.Object,java.lang.String,kotlin.jvm.functions.Function1):V -- f:getSnapshotOrEmpty():java.lang.Object -f:com.intellij.platform.ai.agent.core.extensions.SuspendingOverridableValue -- (kotlin.jvm.functions.Function0):V -- f:value():java.lang.Object -- f:withOverride(java.lang.Object,kotlin.jvm.functions.Function1,kotlin.coroutines.Continuation):java.lang.Object -f:com.intellij.platform.ai.agent.core.session.AgentSessionArchiveTitlesKt -- sf:AGENT_SESSION_ARCHIVED_THREAD_TITLE_PREFIX:java.lang.String -- sf:buildAgentSessionArchivedThreadTitle(java.lang.String,java.lang.String):java.lang.String -- sf:isAgentSessionArchivedThreadTitle(java.lang.String):Z -- sf:normalizeAgentSessionStoredThreadTitle(java.lang.String):java.lang.String -- sf:resolveAgentSessionArchivedTitleState(java.lang.String,java.lang.String):com.intellij.platform.ai.agent.core.session.AgentSessionArchivedTitleState -- sf:stripAgentSessionArchivedThreadTitlePrefix(java.lang.String):java.lang.String -f:com.intellij.platform.ai.agent.core.session.AgentSessionArchivedTitleState -- f:archived:Z -- f:title:java.lang.String -- (java.lang.String,Z):V -- f:component1():java.lang.String -- f:component2():Z -- f:copy(java.lang.String,Z):com.intellij.platform.ai.agent.core.session.AgentSessionArchivedTitleState -- bs:copy$default(com.intellij.platform.ai.agent.core.session.AgentSessionArchivedTitleState,java.lang.String,Z,I,java.lang.Object):com.intellij.platform.ai.agent.core.session.AgentSessionArchivedTitleState -- equals(java.lang.Object):Z -- hashCode():I -f:com.intellij.platform.ai.agent.core.session.AgentSessionCost -- f:amountUsd:java.math.BigDecimal -- f:kind:com.intellij.platform.ai.agent.core.session.AgentSessionCostKind -- f:matchedModelId:java.lang.String -- (java.math.BigDecimal,com.intellij.platform.ai.agent.core.session.AgentSessionCostKind,java.lang.String):V -- b:(java.math.BigDecimal,com.intellij.platform.ai.agent.core.session.AgentSessionCostKind,java.lang.String,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- f:component1():java.math.BigDecimal -- f:component2():com.intellij.platform.ai.agent.core.session.AgentSessionCostKind -- f:component3():java.lang.String -- f:copy(java.math.BigDecimal,com.intellij.platform.ai.agent.core.session.AgentSessionCostKind,java.lang.String):com.intellij.platform.ai.agent.core.session.AgentSessionCost -- bs:copy$default(com.intellij.platform.ai.agent.core.session.AgentSessionCost,java.math.BigDecimal,com.intellij.platform.ai.agent.core.session.AgentSessionCostKind,java.lang.String,I,java.lang.Object):com.intellij.platform.ai.agent.core.session.AgentSessionCost -- equals(java.lang.Object):Z -- hashCode():I -e:com.intellij.platform.ai.agent.core.session.AgentSessionCostKind -- java.lang.Enum -- sf:ESTIMATED:com.intellij.platform.ai.agent.core.session.AgentSessionCostKind -- sf:EXACT:com.intellij.platform.ai.agent.core.session.AgentSessionCostKind -- sf:UNAVAILABLE:com.intellij.platform.ai.agent.core.session.AgentSessionCostKind -- s:getEntries():kotlin.enums.EnumEntries -- s:valueOf(java.lang.String):com.intellij.platform.ai.agent.core.session.AgentSessionCostKind -- s:values():com.intellij.platform.ai.agent.core.session.AgentSessionCostKind[] -e:com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode -- java.lang.Enum -- sf:STANDARD:com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode -- sf:YOLO:com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode -- s:getEntries():kotlin.enums.EnumEntries -- s:valueOf(java.lang.String):com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode -- s:values():com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode[] -f:com.intellij.platform.ai.agent.core.session.AgentSessionModelsKt -- sf:agentSessionOutlinePhaseTitle(java.lang.String,I):java.lang.String -- bs:agentSessionOutlinePhaseTitle$default(java.lang.String,I,I,java.lang.Object):java.lang.String -- sf:buildAgentSessionOutlineTree(java.util.List):java.util.List -- sf:compactAgentSessionOutlineText(java.lang.String,I):java.lang.String -- sf:dedupeAgentSessionOutlineText(java.lang.String):java.lang.String -- sf:normalizeAgentSessionOutlinePreview(java.lang.String,I):java.lang.String -- bs:normalizeAgentSessionOutlinePreview$default(java.lang.String,I,I,java.lang.Object):java.lang.String -- sf:summarizeAgentSessionOutlineChildren(java.util.List):java.lang.String -f:com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItem -- f:children:java.util.List -- f:id:java.lang.String -- f:kind:com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind -- f:preview:java.lang.String -- f:timestampMs:java.lang.Long -- f:title:java.lang.String -- (java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind,java.lang.String,java.lang.String,java.lang.Long,java.util.List):V -- b:(java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind,java.lang.String,java.lang.String,java.lang.Long,java.util.List,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- f:component1():java.lang.String -- f:component2():com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind -- f:component3():java.lang.String -- f:component4():java.lang.String -- f:component5():java.lang.Long -- f:component6():java.util.List -- f:copy(java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind,java.lang.String,java.lang.String,java.lang.Long,java.util.List):com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItem -- bs:copy$default(com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItem,java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind,java.lang.String,java.lang.String,java.lang.Long,java.util.List,I,java.lang.Object):com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItem -- equals(java.lang.Object):Z -- f:getTimestampMillis():java.lang.Long -- hashCode():I -f:com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemBuilder -- f:children:java.util.List -- f:id:java.lang.String -- kind:com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind -- preview:java.lang.String -- f:summarizesChildren:Z -- f:timestampMs:java.lang.Long -- title:java.lang.String -- (java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind,java.lang.String,java.lang.String,java.lang.Long,Z,java.util.List):V -- b:(java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind,java.lang.String,java.lang.String,java.lang.Long,Z,java.util.List,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- f:build():com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItem -e:com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind -- java.lang.Enum -- sf:AGENT_WORK:com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind -- sf:APPROVAL_REQUEST:com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind -- sf:ASSISTANT_RESPONSE:com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind -- sf:INPUT_REQUEST:com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind -- sf:METADATA:com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind -- sf:PLAN:com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind -- sf:SUMMARY:com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind -- sf:TOOL_CALL:com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind -- sf:TOOL_RESULT:com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind -- sf:USER_PROMPT:com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind -- s:getEntries():kotlin.enums.EnumEntries -- s:valueOf(java.lang.String):com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind -- s:values():com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind[] -f:com.intellij.platform.ai.agent.core.session.AgentSessionOutlineTreeRecord -- f:id:java.lang.String -- f:kind:com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind -- f:parentId:java.lang.String -- f:preview:java.lang.String -- f:timestampMs:java.lang.Long -- f:title:java.lang.String -- f:visible:Z -- (java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind,java.lang.String,java.lang.String,java.lang.Long,Z):V -- b:(java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind,java.lang.String,java.lang.String,java.lang.Long,Z,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- f:component1():java.lang.String -- f:component2():java.lang.String -- f:component3():com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind -- f:component4():java.lang.String -- f:component5():java.lang.String -- f:component6():java.lang.Long -- f:component7():Z -- f:copy(java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind,java.lang.String,java.lang.String,java.lang.Long,Z):com.intellij.platform.ai.agent.core.session.AgentSessionOutlineTreeRecord -- bs:copy$default(com.intellij.platform.ai.agent.core.session.AgentSessionOutlineTreeRecord,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind,java.lang.String,java.lang.String,java.lang.Long,Z,I,java.lang.Object):com.intellij.platform.ai.agent.core.session.AgentSessionOutlineTreeRecord -- equals(java.lang.Object):Z -- hashCode():I -f:com.intellij.platform.ai.agent.core.session.AgentSessionProvider -- sf:Companion:com.intellij.platform.ai.agent.core.session.AgentSessionProvider$Companion -- bsf:box-impl(java.lang.String):com.intellij.platform.ai.agent.core.session.AgentSessionProvider -- equals(java.lang.Object):Z -- s:equals-impl(java.lang.String,java.lang.Object):Z -- sf:equals-impl0(java.lang.String,java.lang.String):Z -- f:getValue():java.lang.String -- hashCode():I -- s:hashCode-impl(java.lang.String):I -- s:toString-impl(java.lang.String):java.lang.String -- bf:unbox-impl():java.lang.String -f:com.intellij.platform.ai.agent.core.session.AgentSessionProvider$Companion -- f:from-V7M2pUA(java.lang.String):java.lang.String -- f:fromOrNull-6Etz2MU(java.lang.String):java.lang.String -f:com.intellij.platform.ai.agent.core.session.AgentSessionThread -- f:activityReport:com.intellij.platform.ai.agent.core.AgentThreadActivityReport -- f:archived:Z -- f:cost:com.intellij.platform.ai.agent.core.session.AgentSessionCost -- f:id:java.lang.String -- f:originBranch:java.lang.String -- 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.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 -- f:component3():J -- f:component4():Z -- f:component5():com.intellij.platform.ai.agent.core.AgentThreadActivityReport -- f:component6-dBzBBrw():java.lang.String -- f:component7():java.util.List -- f:component8():java.lang.String -- f:component9():com.intellij.platform.ai.agent.core.session.AgentSessionCost -- 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:getProvider-dBzBBrw():java.lang.String -- hashCode():I -f:com.intellij.platform.ai.agent.core.session.AgentSessionThreadOutline -- f:items:java.util.List -- f:threadId:java.lang.String -- f:title:java.lang.String -- f:updatedAt:J -- f:component1-dBzBBrw():java.lang.String -- f:component2():java.lang.String -- f:component3():java.lang.String -- f:component4():J -- f:component5():java.util.List -- f:copy-fQRq96w(java.lang.String,java.lang.String,java.lang.String,J,java.util.List):com.intellij.platform.ai.agent.core.session.AgentSessionThreadOutline -- bs:copy-fQRq96w$default(com.intellij.platform.ai.agent.core.session.AgentSessionThreadOutline,java.lang.String,java.lang.String,java.lang.String,J,java.util.List,I,java.lang.Object):com.intellij.platform.ai.agent.core.session.AgentSessionThreadOutline -- equals(java.lang.Object):Z -- f:getProvider-dBzBBrw():java.lang.String -- f:getSessionId():java.lang.String -- hashCode():I -f:com.intellij.platform.ai.agent.core.session.AgentSubAgent -- f:activity:com.intellij.platform.ai.agent.core.AgentThreadActivity -- f:id:java.lang.String -- f:name:java.lang.String -- (java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.AgentThreadActivity):V -- b:(java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.AgentThreadActivity,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- f:component1():java.lang.String -- f:component2():java.lang.String -- f:component3():com.intellij.platform.ai.agent.core.AgentThreadActivity -- f:copy(java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.AgentThreadActivity):com.intellij.platform.ai.agent.core.session.AgentSubAgent -- bs:copy$default(com.intellij.platform.ai.agent.core.session.AgentSubAgent,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.AgentThreadActivity,I,java.lang.Object):com.intellij.platform.ai.agent.core.session.AgentSubAgent -- equals(java.lang.Object):Z -- hashCode():I diff --git a/plugins/agent-workbench/lib-agent/core/exposed-third-party-api.txt b/plugins/agent-workbench/lib-agent/core/exposed-third-party-api.txt deleted file mode 100644 index ee686436f8d7..000000000000 --- a/plugins/agent-workbench/lib-agent/core/exposed-third-party-api.txt +++ /dev/null @@ -1,2 +0,0 @@ -java/math/BigDecimal -kotlin/jvm/internal/DefaultConstructorMarker diff --git a/plugins/agent-workbench/lib-agent/core/intellij.platform.ai.agent.core.iml b/plugins/agent-workbench/lib-agent/core/intellij.platform.ai.agent.core.iml deleted file mode 100644 index 268f5e4ebc8f..000000000000 --- a/plugins/agent-workbench/lib-agent/core/intellij.platform.ai.agent.core.iml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/plugins/agent-workbench/lib-agent/core/intellij.platform.ai.agent.core.tests.iml b/plugins/agent-workbench/lib-agent/core/intellij.platform.ai.agent.core.tests.iml deleted file mode 100644 index 6f1d307f4497..000000000000 --- a/plugins/agent-workbench/lib-agent/core/intellij.platform.ai.agent.core.tests.iml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/plugins/agent-workbench/lib-agent/core/resources/intellij.platform.ai.agent.core.xml b/plugins/agent-workbench/lib-agent/core/resources/intellij.platform.ai.agent.core.xml deleted file mode 100644 index 6fb76a66265d..000000000000 --- a/plugins/agent-workbench/lib-agent/core/resources/intellij.platform.ai.agent.core.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/plugins/agent-workbench/lib-agent/core/src/AgentThreadActivity.kt b/plugins/agent-workbench/lib-agent/core/src/AgentThreadActivity.kt deleted file mode 100644 index ab889b3178dd..000000000000 --- a/plugins/agent-workbench/lib-agent/core/src/AgentThreadActivity.kt +++ /dev/null @@ -1,51 +0,0 @@ -// 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.core - -import org.jetbrains.annotations.ApiStatus - -enum class AgentThreadActivity { - READY, - PROCESSING, - REVIEWING, - NEEDS_INPUT, - UNREAD, -} - -data class AgentThreadActivityReport( - @JvmField val rowActivity: AgentThreadActivity, - @JvmField val chromeActivity: AgentThreadActivity? = rowActivity, -) { - companion object { - @JvmField - val READY: AgentThreadActivityReport = AgentThreadActivityReport(AgentThreadActivity.READY) - } -} - -enum class AgentThreadActivityBucket { - ATTENTION, - RUNNING, - DONE, -} - -val AgentThreadActivity.isWorking: Boolean - get() = this == AgentThreadActivity.PROCESSING || this == AgentThreadActivity.REVIEWING - -fun AgentThreadActivityReport.chromeBucket(): AgentThreadActivityBucket? { - return when (chromeActivity) { - AgentThreadActivity.NEEDS_INPUT, - AgentThreadActivity.REVIEWING, - -> AgentThreadActivityBucket.ATTENTION - - AgentThreadActivity.PROCESSING -> AgentThreadActivityBucket.RUNNING - AgentThreadActivity.UNREAD -> AgentThreadActivityBucket.DONE - AgentThreadActivity.READY, - null, - -> null - } -} - -@ApiStatus.Internal -fun AgentThreadActivityReport.chromePresentationActivity(): AgentThreadActivity { - // Chrome consumers need a concrete badge state; null chrome activity means no badge. - return chromeActivity ?: AgentThreadActivity.READY -} diff --git a/plugins/agent-workbench/lib-agent/core/src/AgentThreadIdentity.kt b/plugins/agent-workbench/lib-agent/core/src/AgentThreadIdentity.kt deleted file mode 100644 index 393af5e25490..000000000000 --- a/plugins/agent-workbench/lib-agent/core/src/AgentThreadIdentity.kt +++ /dev/null @@ -1,38 +0,0 @@ -// 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.core - -private val PROVIDER_ID_REGEX = Regex("[a-z][a-z0-9._-]*") - -data class AgentThreadIdentity( - @JvmField val providerId: String, - @JvmField val threadId: String, -) - -fun buildAgentThreadIdentity(providerId: String, threadId: String): String { - require(PROVIDER_ID_REGEX.matches(providerId)) { - "Invalid provider id '$providerId'. Expected: ${PROVIDER_ID_REGEX.pattern}" - } - require(threadId.isNotBlank()) { - "Thread id must not be blank" - } - return "$providerId:$threadId" -} - -fun parseAgentThreadIdentity(identity: String): AgentThreadIdentity? { - val separator = identity.indexOf(':') - if (separator <= 0 || separator == identity.lastIndex) { - return null - } - - val providerId = identity.substring(0, separator) - if (!PROVIDER_ID_REGEX.matches(providerId)) { - return null - } - - val threadId = identity.substring(separator + 1) - if (threadId.isBlank()) { - return null - } - - return AgentThreadIdentity(providerId = providerId, threadId = threadId) -} diff --git a/plugins/agent-workbench/lib-agent/core/src/AgentWorkbenchPathUtil.kt b/plugins/agent-workbench/lib-agent/core/src/AgentWorkbenchPathUtil.kt deleted file mode 100644 index 1ee4346e606e..000000000000 --- a/plugins/agent-workbench/lib-agent/core/src/AgentWorkbenchPathUtil.kt +++ /dev/null @@ -1,40 +0,0 @@ -// 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.core - -import java.nio.file.InvalidPathException -import java.nio.file.Path -import kotlin.io.path.invariantSeparatorsPathString - -fun parseAgentWorkbenchPathOrNull(path: String): Path? { - return try { - Path.of(path) - } - catch (_: InvalidPathException) { - null - } -} - -fun normalizeAgentWorkbenchPath(path: String): String { - return parseAgentWorkbenchPathOrNull(path)?.invariantSeparatorsPathString ?: path -} - -fun normalizeAgentWorkbenchPathOrNull(path: String): String? { - return parseAgentWorkbenchPathOrNull(path)?.invariantSeparatorsPathString -} - -fun normalizeAgentSessionProjectPath(path: String): String? { - val trimmedPath = path.trim().takeIf { it.isNotEmpty() } ?: return null - val normalizedPath = normalizeAgentWorkbenchPathOrNull(trimmedPath) ?: return null - return normalizedPath.trimEnd('/').ifEmpty { "/" } -} - -fun normalizeAgentSessionTitle(title: String?): String? { - return title - ?.replace('\n', ' ') - ?.replace('\r', ' ') - ?.replace(AGENT_SESSION_TITLE_WHITESPACE, " ") - ?.trim() - ?.takeIf { it.isNotEmpty() } -} - -private val AGENT_SESSION_TITLE_WHITESPACE = Regex("\\s+") diff --git a/plugins/agent-workbench/lib-agent/core/src/extensions/AgentWorkbenchExtensionPointSupport.kt b/plugins/agent-workbench/lib-agent/core/src/extensions/AgentWorkbenchExtensionPointSupport.kt deleted file mode 100644 index 158e8644bc3c..000000000000 --- a/plugins/agent-workbench/lib-agent/core/src/extensions/AgentWorkbenchExtensionPointSupport.kt +++ /dev/null @@ -1,122 +0,0 @@ -// 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.core.extensions - -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.extensions.ExtensionPointName -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock - -private sealed interface OverrideState { - data object Unset : OverrideState - - data class Value(val value: T) : OverrideState -} - -class OverridableValue( - private val fallback: () -> T, -) { - private val lock = Any() - - @Volatile - private var overrideState: OverrideState = OverrideState.Unset - - fun value(): T { - return when (val current = overrideState) { - OverrideState.Unset -> fallback() - is OverrideState.Value -> current.value - } - } - - fun withOverride(value: T, action: () -> R): R { - return synchronized(lock) { - val previous = overrideState - overrideState = OverrideState.Value(value) - try { - action() - } - finally { - overrideState = previous - } - } - } -} - -class SuspendingOverridableValue( - private val fallback: () -> T, -) { - private val mutex = Mutex() - - @Volatile - private var overrideState: OverrideState = OverrideState.Unset - - fun value(): T { - return when (val current = overrideState) { - OverrideState.Unset -> fallback() - is OverrideState.Value -> current.value - } - } - - suspend fun withOverride(value: T, action: suspend () -> R): R { - return mutex.withLock { - val previous = overrideState - overrideState = OverrideState.Value(value) - try { - action() - } - finally { - overrideState = previous - } - } - } -} - -class SnapshotExtensionPointCache( - private val log: Logger, - private val extensionPoint: ExtensionPointName, - private val cacheId: Class, - private val emptySnapshot: S, - private val unavailableMessage: String, - private val buildSnapshot: (Iterable) -> S, -) { - fun getSnapshotOrEmpty(): S { - return try { - extensionPoint.computeIfAbsent(cacheId) { - buildSnapshot(extensionPoint.extensionList) - } - } - catch (t: IllegalStateException) { - log.debug(unavailableMessage, t) - emptySnapshot - } - catch (t: IllegalArgumentException) { - log.debug(unavailableMessage, t) - emptySnapshot - } - } -} - -class SingleExtensionPointResolver( - private val log: Logger, - private val extensionPoint: ExtensionPointName, - private val unavailableMessage: String, - private val multipleExtensionsMessage: (List) -> String, -) { - fun findFirstOrNull(): T? { - val extensions = try { - extensionPoint.extensionList - } - catch (t: IllegalStateException) { - log.debug(unavailableMessage, t) - return null - } - catch (t: IllegalArgumentException) { - log.debug(unavailableMessage, t) - return null - } - - if (extensions.size > 1) { - log.warn(multipleExtensionsMessage(extensions)) - } - return extensions.firstOrNull() - } -} diff --git a/plugins/agent-workbench/lib-agent/core/src/session/AgentSessionArchiveTitles.kt b/plugins/agent-workbench/lib-agent/core/src/session/AgentSessionArchiveTitles.kt deleted file mode 100644 index 53e47e7900bf..000000000000 --- a/plugins/agent-workbench/lib-agent/core/src/session/AgentSessionArchiveTitles.kt +++ /dev/null @@ -1,38 +0,0 @@ -// 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.core.session - -const val AGENT_SESSION_ARCHIVED_THREAD_TITLE_PREFIX: String = "[archived] " - -data class AgentSessionArchivedTitleState( - @JvmField val title: String, - @JvmField val archived: Boolean, -) - -fun resolveAgentSessionArchivedTitleState(rawTitle: String, defaultTitle: String): AgentSessionArchivedTitleState { - val archived = isAgentSessionArchivedThreadTitle(rawTitle) - val visibleTitle = normalizeAgentSessionStoredThreadTitle(stripAgentSessionArchivedThreadTitlePrefix(rawTitle)) - ?: defaultTitle - return AgentSessionArchivedTitleState(title = visibleTitle, archived = archived) -} - -fun buildAgentSessionArchivedThreadTitle(title: String, defaultTitle: String): String { - return AGENT_SESSION_ARCHIVED_THREAD_TITLE_PREFIX + resolveAgentSessionArchivedTitleState(title, defaultTitle).title -} - -fun isAgentSessionArchivedThreadTitle(title: String): Boolean { - return title.startsWith(AGENT_SESSION_ARCHIVED_THREAD_TITLE_PREFIX) -} - -fun stripAgentSessionArchivedThreadTitlePrefix(title: String): String { - return if (isAgentSessionArchivedThreadTitle(title)) title.removePrefix(AGENT_SESSION_ARCHIVED_THREAD_TITLE_PREFIX) else title -} - -fun normalizeAgentSessionStoredThreadTitle(value: String?): String? { - val normalized = value - ?.replace('\n', ' ') - ?.replace('\r', ' ') - ?.replace(Regex("\\s+"), " ") - ?.trim() - ?: return null - return normalized.takeIf(String::isNotEmpty) -} diff --git a/plugins/agent-workbench/lib-agent/core/src/session/AgentSessionCost.kt b/plugins/agent-workbench/lib-agent/core/src/session/AgentSessionCost.kt deleted file mode 100644 index f95fd39a032f..000000000000 --- a/plugins/agent-workbench/lib-agent/core/src/session/AgentSessionCost.kt +++ /dev/null @@ -1,16 +0,0 @@ -// 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.core.session - -import java.math.BigDecimal - -enum class AgentSessionCostKind { - EXACT, - ESTIMATED, - UNAVAILABLE, -} - -data class AgentSessionCost( - @JvmField val amountUsd: BigDecimal?, - @JvmField val kind: AgentSessionCostKind, - @JvmField val matchedModelId: String? = null, -) diff --git a/plugins/agent-workbench/lib-agent/core/src/session/AgentSessionModels.kt b/plugins/agent-workbench/lib-agent/core/src/session/AgentSessionModels.kt deleted file mode 100644 index 8199469ad2f3..000000000000 --- a/plugins/agent-workbench/lib-agent/core/src/session/AgentSessionModels.kt +++ /dev/null @@ -1,288 +0,0 @@ -// 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.core.session - -// @spec plugins/air/spec/sessions/agent-terminal-sessions.spec.md - -import com.intellij.platform.ai.agent.core.AgentThreadActivity -import com.intellij.platform.ai.agent.core.AgentThreadActivityReport -import com.intellij.openapi.util.NlsSafe -import java.util.IdentityHashMap - -private val AGENT_SESSION_PROVIDER_ID_REGEX = Regex("[a-z][a-z0-9._-]*") -private val AGENT_SESSION_OUTLINE_WHITESPACE = Regex("\\s+") - -@JvmInline -value class AgentSessionProvider private constructor(val value: String) { - companion object { - fun from(value: String): AgentSessionProvider { - require(AGENT_SESSION_PROVIDER_ID_REGEX.matches(value)) { - "Invalid provider id '$value'. Expected: ${AGENT_SESSION_PROVIDER_ID_REGEX.pattern}" - } - return AgentSessionProvider(value) - } - - fun fromOrNull(value: String): AgentSessionProvider? { - return if (AGENT_SESSION_PROVIDER_ID_REGEX.matches(value)) AgentSessionProvider(value) else null - } - } - - override fun toString(): String = value -} - -enum class AgentSessionLaunchMode { - STANDARD, - YOLO, -} - -data class AgentSubAgent( - @JvmField val id: @NlsSafe String, - @JvmField val name: @NlsSafe String, - @JvmField val activity: AgentThreadActivity = AgentThreadActivity.READY, -) - -data class AgentSessionThread( - @JvmField val id: String, - @JvmField val title: String, - @JvmField val updatedAt: Long, - @JvmField val archived: Boolean, - @JvmField val activityReport: AgentThreadActivityReport = AgentThreadActivityReport.READY, - val provider: AgentSessionProvider, - @JvmField val subAgents: List = emptyList(), - @JvmField val originBranch: String? = null, - @JvmField val cost: AgentSessionCost? = null, -) - -/** - * Provider-neutral outline for a persisted agent thread. - * - * This is a role-aware conversation outline, not a dump of provider-specific log records. [items] are the visible roots in provider - * history order. Providers should hide bookkeeping containers and expose user prompts, assistant responses, tool activity, plans, - * approvals, and summaries with the closest [AgentSessionOutlineItemKind]. A `null` outline from a source means the provider cannot - * load an outline for the thread; an empty [items] list means the outline is supported but has no visible entries. - */ -data class AgentSessionThreadOutline( - val provider: AgentSessionProvider, - @JvmField val threadId: String, - @JvmField val title: String, - @JvmField val updatedAt: Long, - @JvmField val items: List, -) { - val sessionId: String - get() = threadId -} - -/** - * Visible item in an [AgentSessionThreadOutline]. - * - * [id] must be unique within the outline and stable across reloads for the same provider record. If navigation or fork actions are - * enabled for this item, [id] must be a provider/backend anchor rather than a UI ordinal. [kind] is semantic role data and should be - * mapped from native provider roles, not chosen for presentation only. [title] is the primary row label; [preview] is supporting text - * and may contain the message body when the title is intentionally blank. [children] describe ownership or causality, for example tool - * calls and results under the assistant/work item that produced them. - */ -data class AgentSessionOutlineItem( - @JvmField val id: String, - @JvmField val kind: AgentSessionOutlineItemKind, - @JvmField val title: String, - @JvmField val preview: String? = null, - @JvmField val timestampMs: Long? = null, - @JvmField val children: List = emptyList(), -) { - val timestampMillis: Long? - get() = timestampMs -} - -/** - * Semantic role of an [AgentSessionOutlineItem]. - * - * Providers should map native transcript roles to these values consistently so shared UI and actions can reason about the outline: - * user-authored prompts use [USER_PROMPT], assistant text uses [ASSISTANT_RESPONSE], provider processing phases use [AGENT_WORK], - * invocations use [TOOL_CALL], their outputs use [TOOL_RESULT], and internal or unknown records should be skipped or mapped to - * [METADATA] instead of leaking raw provider event names. - */ -enum class AgentSessionOutlineItemKind { - USER_PROMPT, - ASSISTANT_RESPONSE, - AGENT_WORK, - TOOL_CALL, - TOOL_RESULT, - PLAN, - APPROVAL_REQUEST, - INPUT_REQUEST, - SUMMARY, - METADATA, -} - -/** Mutable helper for constructing nested [AgentSessionOutlineItem] trees. */ -class AgentSessionOutlineItemBuilder( - @JvmField val id: String, - @JvmField var kind: AgentSessionOutlineItemKind, - @JvmField var title: String, - @JvmField var preview: String?, - @JvmField val timestampMs: Long?, - @JvmField val summarizesChildren: Boolean = false, - @JvmField val children: MutableList = ArrayList(), -) { - fun build(): AgentSessionOutlineItem { - val builtChildren = children.map(AgentSessionOutlineItemBuilder::build) - return AgentSessionOutlineItem( - id = id, - kind = kind, - title = title, - preview = if (summarizesChildren && builtChildren.isNotEmpty()) summarizeAgentSessionOutlineChildren(builtChildren) else preview, - timestampMs = timestampMs, - children = builtChildren, - ) - } -} - -/** - * Flat source record used by [buildAgentSessionOutlineTree]. - * - * [parentId] links provider records before they are converted to visible outline items. Set [visible] to `false` for bookkeeping - * containers such as raw turns or sessions; their visible descendants are promoted to the nearest visible ancestor or to the root. - * Records with missing, blank, or self-referential parents become roots. Duplicate ids keep the first record. - */ -data class AgentSessionOutlineTreeRecord( - @JvmField val id: String, - @JvmField val parentId: String?, - @JvmField val kind: AgentSessionOutlineItemKind, - @JvmField val title: String, - @JvmField val preview: String? = null, - @JvmField val timestampMs: Long? = null, - @JvmField val visible: Boolean = true, -) - -/** - * Builds a visible outline tree from flat provider records. - * - * Returned roots and siblings are ordered by [AgentSessionOutlineTreeRecord.timestampMs] when present, then by input order. Invisible - * records are not returned, but their visible descendants are preserved and promoted. This lets providers keep raw parent/child - * relationships for future navigation while presenting a flat, role-aware conversation timeline. - */ -fun buildAgentSessionOutlineTree(records: List): List { - if (records.isEmpty()) { - return emptyList() - } - - val nodesById = LinkedHashMap(records.size) - records.forEachIndexed { index, record -> - val id = record.id.trim().takeIf { it.isNotEmpty() } ?: return@forEachIndexed - nodesById.putIfAbsent(id, AgentSessionOutlineTreeNode(record = record, order = index)) - } - - val roots = ArrayList() - for (node in nodesById.values) { - val parentId = node.record.parentId?.trim()?.takeIf { it.isNotEmpty() && it != node.record.id } - val parent = parentId?.let(nodesById::get) - if (parent == null) { - roots += node - } - else { - parent.children += node - } - } - return buildVisibleOutlineItems(roots) -} - -fun normalizeAgentSessionOutlinePreview(value: String?, maxLength: Int = 160): String? { - val normalized = value - ?.replace('\n', ' ') - ?.replace('\r', ' ') - ?.replace(AGENT_SESSION_OUTLINE_WHITESPACE, " ") - ?.trim() - ?.takeIf { it.isNotEmpty() } - ?: return null - return if (normalized.length <= maxLength) normalized else normalized.take(maxLength - 3).trimEnd() + "..." -} - -fun compactAgentSessionOutlineText(value: String?, maxLength: Int): String? { - return normalizeAgentSessionOutlinePreview(value, maxLength = maxLength) -} - -fun agentSessionOutlinePhaseTitle(preview: String?, maxLength: Int = 90): String? { - val normalizedPreview = normalizeAgentSessionOutlinePreview(preview) ?: return null - val sentenceEnd = normalizedPreview.indexOfAny(charArrayOf('.', '!', '?')) - val firstSentence = if (sentenceEnd > 0) normalizedPreview.take(sentenceEnd + 1) else normalizedPreview - return firstSentence.take(maxLength).trimEnd().takeIf { it.isNotEmpty() } -} - -fun dedupeAgentSessionOutlineText(value: String): String { - return value.replace(AGENT_SESSION_OUTLINE_WHITESPACE, " ").trim().lowercase() -} - -fun summarizeAgentSessionOutlineChildren(children: List): String? { - val parts = ArrayList(6) - addAgentSessionOutlineCount(parts, children.count { it.kind == AgentSessionOutlineItemKind.TOOL_CALL }, "tool") - addAgentSessionOutlineCount(parts, children.count { it.kind == AgentSessionOutlineItemKind.TOOL_RESULT }, "result") - addAgentSessionOutlineCount(parts, children.count { it.kind == AgentSessionOutlineItemKind.PLAN }, "plan") - addAgentSessionOutlineCount(parts, children.count { it.kind == AgentSessionOutlineItemKind.APPROVAL_REQUEST }, "approval") - addAgentSessionOutlineCount(parts, children.count { it.kind == AgentSessionOutlineItemKind.INPUT_REQUEST }, "input request") - addAgentSessionOutlineCount(parts, children.count { it.kind == AgentSessionOutlineItemKind.AGENT_WORK }, "work item") - return parts.joinToString(", ").takeIf { it.isNotEmpty() } -} - -private fun buildVisibleOutlineItems(roots: List): List { - val builtByNode = IdentityHashMap>() - val stack = ArrayDeque() - roots.sortedWith(AGENT_SESSION_OUTLINE_TREE_NODE_COMPARATOR).asReversed().forEach { root -> - stack.addLast(AgentSessionOutlineTreeStackFrame(root, childrenVisited = false)) - } - - while (stack.isNotEmpty()) { - val frame = stack.removeLast() - val node = frame.node - if (!frame.childrenVisited) { - stack.addLast(AgentSessionOutlineTreeStackFrame(node, childrenVisited = true)) - node.children.sortedWith(AGENT_SESSION_OUTLINE_TREE_NODE_COMPARATOR).asReversed().forEach { child -> - stack.addLast(AgentSessionOutlineTreeStackFrame(child, childrenVisited = false)) - } - continue - } - - val builtChildren = node.children.sortedWith(AGENT_SESSION_OUTLINE_TREE_NODE_COMPARATOR) - .flatMap { child -> builtByNode.remove(child).orEmpty() } - val record = node.record - builtByNode[node] = if (record.visible) { - listOf( - AgentSessionOutlineItem( - id = record.id, - kind = record.kind, - title = record.title, - preview = normalizeAgentSessionOutlinePreview(record.preview), - timestampMs = record.timestampMs, - children = builtChildren, - ) - ) - } - else { - builtChildren - } - } - - return roots.sortedWith(AGENT_SESSION_OUTLINE_TREE_NODE_COMPARATOR) - .flatMap { root -> builtByNode[root].orEmpty() } -} - -private fun addAgentSessionOutlineCount(parts: MutableList, count: Int, label: String) { - if (count <= 0) { - return - } - parts += "$count ${if (count == 1) label else label + "s"}" -} - -private data class AgentSessionOutlineTreeNode( - @JvmField val record: AgentSessionOutlineTreeRecord, - @JvmField val order: Int, - @JvmField val children: MutableList = ArrayList(), -) - -private data class AgentSessionOutlineTreeStackFrame( - @JvmField val node: AgentSessionOutlineTreeNode, - @JvmField val childrenVisited: Boolean, -) - -private val AGENT_SESSION_OUTLINE_TREE_NODE_COMPARATOR = compareBy( - { node -> node.record.timestampMs ?: Long.MAX_VALUE }, - { node -> node.order }, -) diff --git a/plugins/agent-workbench/lib-agent/core/testSrc/AgentSessionArchiveTitlesTest.kt b/plugins/agent-workbench/lib-agent/core/testSrc/AgentSessionArchiveTitlesTest.kt deleted file mode 100644 index d072849231c1..000000000000 --- a/plugins/agent-workbench/lib-agent/core/testSrc/AgentSessionArchiveTitlesTest.kt +++ /dev/null @@ -1,34 +0,0 @@ -// 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.core - -import com.intellij.platform.ai.agent.core.session.AGENT_SESSION_ARCHIVED_THREAD_TITLE_PREFIX -import com.intellij.platform.ai.agent.core.session.buildAgentSessionArchivedThreadTitle -import com.intellij.platform.ai.agent.core.session.resolveAgentSessionArchivedTitleState -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test - -class AgentSessionArchiveTitlesTest { - @Test - fun `archived title prefix is stripped from visible title`() { - val state = resolveAgentSessionArchivedTitleState("${AGENT_SESSION_ARCHIVED_THREAD_TITLE_PREFIX}Fix tests", defaultTitle = "Fallback") - - assertThat(state.archived).isTrue() - assertThat(state.title).isEqualTo("Fix tests") - } - - @Test - fun `missing visible title falls back to default`() { - val state = resolveAgentSessionArchivedTitleState(AGENT_SESSION_ARCHIVED_THREAD_TITLE_PREFIX, defaultTitle = "Fallback") - - assertThat(state.archived).isTrue() - assertThat(state.title).isEqualTo("Fallback") - } - - @Test - fun `building archived title does not duplicate prefix`() { - val archivedTitle = - buildAgentSessionArchivedThreadTitle("${AGENT_SESSION_ARCHIVED_THREAD_TITLE_PREFIX}Fix tests", defaultTitle = "Fallback") - - assertThat(archivedTitle).isEqualTo("${AGENT_SESSION_ARCHIVED_THREAD_TITLE_PREFIX}Fix tests") - } -} diff --git a/plugins/agent-workbench/lib-agent/core/testSrc/AgentSessionOutlineTreeTest.kt b/plugins/agent-workbench/lib-agent/core/testSrc/AgentSessionOutlineTreeTest.kt deleted file mode 100644 index 36788fda04c1..000000000000 --- a/plugins/agent-workbench/lib-agent/core/testSrc/AgentSessionOutlineTreeTest.kt +++ /dev/null @@ -1,59 +0,0 @@ -// 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.core - -import com.intellij.platform.ai.agent.core.session.AgentSessionOutlineItemKind -import com.intellij.platform.ai.agent.core.session.AgentSessionOutlineTreeRecord -import com.intellij.platform.ai.agent.core.session.buildAgentSessionOutlineTree -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test - -class AgentSessionOutlineTreeTest { - @Test - fun `hidden bookkeeping records promote visible children`() { - val items = buildAgentSessionOutlineTree( - listOf( - record(id = "user", parentId = null, kind = AgentSessionOutlineItemKind.USER_PROMPT, title = "User", timestampMs = 1_000L), - record(id = "label", parentId = "user", visible = false, timestampMs = 2_000L), - record(id = "assistant", - parentId = "label", - kind = AgentSessionOutlineItemKind.ASSISTANT_RESPONSE, - title = "Done", - timestampMs = 3_000L), - ) - ) - - assertThat(items).hasSize(1) - assertThat(items.single().id).isEqualTo("user") - assertThat(items.single().children.map { it.id }).containsExactly("assistant") - } - - @Test - fun `missing and self parents become timestamp ordered roots`() { - val items = buildAgentSessionOutlineTree( - listOf( - record(id = "late", parentId = "missing", title = "Late", timestampMs = 3_000L), - record(id = "early", parentId = "early", title = "Early", timestampMs = 1_000L), - ) - ) - - assertThat(items.map { it.id }).containsExactly("early", "late") - } - - private fun record( - id: String, - parentId: String?, - kind: AgentSessionOutlineItemKind = AgentSessionOutlineItemKind.METADATA, - title: String = "Metadata", - timestampMs: Long?, - visible: Boolean = true, - ): AgentSessionOutlineTreeRecord { - return AgentSessionOutlineTreeRecord( - id = id, - parentId = parentId, - kind = kind, - title = title, - timestampMs = timestampMs, - visible = visible, - ) - } -} diff --git a/plugins/agent-workbench/lib-agent/core/testSrc/AgentThreadIdentityTest.kt b/plugins/agent-workbench/lib-agent/core/testSrc/AgentThreadIdentityTest.kt deleted file mode 100644 index ab3f5fba490a..000000000000 --- a/plugins/agent-workbench/lib-agent/core/testSrc/AgentThreadIdentityTest.kt +++ /dev/null @@ -1,40 +0,0 @@ -// 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.core - -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.assertThatThrownBy -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.Timeout -import java.util.concurrent.TimeUnit - -@Timeout(value = 2, unit = TimeUnit.MINUTES) -class AgentThreadIdentityTest { - @Test - fun buildsAndParsesValidIdentity() { - val identity = buildAgentThreadIdentity(providerId = "codex", threadId = "thread-1") - - assertThat(identity).isEqualTo("codex:thread-1") - assertThat(parseAgentThreadIdentity(identity)).isEqualTo(AgentThreadIdentity(providerId = "codex", threadId = "thread-1")) - } - - @Test - fun rejectsInvalidProviderIdWhenBuildingIdentity() { - assertThatThrownBy { buildAgentThreadIdentity(providerId = "Codex", threadId = "thread-1") } - .isInstanceOf(IllegalArgumentException::class.java) - .hasMessageContaining("Invalid provider id") - } - - @Test - fun rejectsBlankThreadIdWhenBuildingIdentity() { - assertThatThrownBy { buildAgentThreadIdentity(providerId = "codex", threadId = " ") } - .isInstanceOf(IllegalArgumentException::class.java) - .hasMessageContaining("Thread id must not be blank") - } - - @Test - fun rejectsMalformedIdentityWhenParsing() { - assertThat(parseAgentThreadIdentity("codex")).isNull() - assertThat(parseAgentThreadIdentity("Codex:thread-1")).isNull() - assertThat(parseAgentThreadIdentity("codex: ")).isNull() - } -} diff --git a/plugins/agent-workbench/lib-agent/core/testSrc/AgentWorkbenchPathUtilTest.kt b/plugins/agent-workbench/lib-agent/core/testSrc/AgentWorkbenchPathUtilTest.kt deleted file mode 100644 index 3ffe090d1b8b..000000000000 --- a/plugins/agent-workbench/lib-agent/core/testSrc/AgentWorkbenchPathUtilTest.kt +++ /dev/null @@ -1,46 +0,0 @@ -// 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.core - -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.Timeout -import java.nio.file.Path -import java.util.concurrent.TimeUnit - -@Timeout(value = 2, unit = TimeUnit.MINUTES) -class AgentWorkbenchPathUtilTest { - @Test - fun parsesAndNormalizesValidPaths() { - val nativePath = Path.of("workspace", "child").toString() - - assertThat(parseAgentWorkbenchPathOrNull(nativePath)).isEqualTo(Path.of("workspace", "child")) - assertThat(normalizeAgentWorkbenchPath(nativePath)).isEqualTo("workspace/child") - assertThat(normalizeAgentWorkbenchPathOrNull(nativePath)).isEqualTo("workspace/child") - } - - @Test - fun preservesInvalidPathsWhenNormalizationFails() { - val invalidPath = "bad" + '\u0000' + "path" - - assertThat(parseAgentWorkbenchPathOrNull(invalidPath)).isNull() - assertThat(normalizeAgentWorkbenchPath(invalidPath)).isEqualTo(invalidPath) - assertThat(normalizeAgentWorkbenchPathOrNull(invalidPath)).isNull() - } - - @Test - fun normalizesAgentSessionProjectPaths() { - val nativePath = Path.of("workspace", "child").toString() - - assertThat(normalizeAgentSessionProjectPath(" $nativePath/ ")).isEqualTo("workspace/child") - assertThat(normalizeAgentSessionProjectPath("/")).isEqualTo("/") - assertThat(normalizeAgentSessionProjectPath(" ")).isNull() - assertThat(normalizeAgentSessionProjectPath("bad" + '\u0000' + "path")).isNull() - } - - @Test - fun normalizesAgentSessionTitles() { - assertThat(normalizeAgentSessionTitle(" first\nsecond\tthird\r ")).isEqualTo("first second third") - assertThat(normalizeAgentSessionTitle(" ")).isNull() - assertThat(normalizeAgentSessionTitle(null)).isNull() - } -} diff --git a/plugins/agent-workbench/lib-agent/filewatch/BUILD.bazel b/plugins/agent-workbench/lib-agent/filewatch/BUILD.bazel deleted file mode 100644 index 7c0de5ff40ba..000000000000 --- a/plugins/agent-workbench/lib-agent/filewatch/BUILD.bazel +++ /dev/null @@ -1,115 +0,0 @@ -load("@community//build:tests-options.bzl", "jps_test") -load("@rules_jvm//:jvm.bzl", "jvm_library") - -### auto-generated section `build intellij.platform.ai.agent.filewatch` start - -jvm_library( - name = "filewatch", - srcs = glob( - [ - "src/**/*.kt", - "src/**/*.java", - "src/**/*.form", - ], - allow_empty = True, - ), - module_name = "intellij.platform.ai.agent.filewatch", - resource_strip_prefix = "resources", - resources = glob( - ["resources/**/*"], - ), - visibility = ["//visibility:public"], - deps = [ - # do not sort, - "@lib//:kotlin-stdlib", - "//libraries/kotlinx/coroutines/core", - "@lib//:jna", - "//platform/util", - ], -) - -jvm_library( - name = "filewatch_test_lib", - testonly = True, - srcs = glob( - [], - allow_empty = True, - ), - module_name = "intellij.platform.ai.agent.filewatch", - visibility = ["//visibility:public"], - runtime_deps = [ - # do not sort, - ":filewatch", - "//libraries/kotlinx/coroutines/core:core_test_lib", - "//platform/util:util_test_lib", - ], -) -### auto-generated section `build intellij.platform.ai.agent.filewatch` end - -### auto-generated section `iml intellij.platform.ai.agent.filewatch` start -exports_files( - [ - "intellij.platform.ai.agent.filewatch.iml", - ], - visibility = ["//visibility:public"], -) -### auto-generated section `iml intellij.platform.ai.agent.filewatch` end - -### auto-generated section `build intellij.platform.ai.agent.filewatch.tests` start - -jvm_library( - name = "ai-agent-filewatch-tests", - srcs = glob( - [], - allow_empty = True, - ), - module_name = "intellij.platform.ai.agent.filewatch.tests", - visibility = ["//visibility:public"], -) - -jvm_library( - name = "ai-agent-filewatch-tests_test_lib", - testonly = True, - srcs = glob( - [ - "testSrc/**/*.kt", - "testSrc/**/*.java", - "testSrc/**/*.form", - ], - allow_empty = True, - ), - associates = [ - "//plugins/agent-workbench/lib-agent/filewatch", - "//plugins/agent-workbench/lib-agent/filewatch:filewatch_test_lib", - ], - visibility = ["//visibility:public"], - runtime_deps = [":ai-agent-filewatch-tests"], - deps = [ - # do not sort, - "@lib//:kotlin-stdlib", - "//libraries/kotlinx/coroutines/core", - "//libraries/kotlinx/coroutines/core:core_test_lib", - "//libraries/junit5", - "//libraries/junit5:junit5_test_lib", - "//libraries/assertj-core", - "//libraries/assertj-core:assertj-core_test_lib", - ], -) -### auto-generated section `build intellij.platform.ai.agent.filewatch.tests` end - -### auto-generated section `iml intellij.platform.ai.agent.filewatch.tests` start -exports_files( - [ - "intellij.platform.ai.agent.filewatch.tests.iml", - ], - visibility = ["//visibility:public"], -) -### auto-generated section `iml intellij.platform.ai.agent.filewatch.tests` end - -### auto-generated section `test intellij.platform.ai.agent.filewatch.tests` start - -jps_test( - name = "ai-agent-filewatch-tests_test", - runtime_deps = [":ai-agent-filewatch-tests_test_lib"], -) -### auto-generated section `test intellij.platform.ai.agent.filewatch.tests` end diff --git a/plugins/agent-workbench/lib-agent/filewatch/api-dump.txt b/plugins/agent-workbench/lib-agent/filewatch/api-dump.txt deleted file mode 100644 index f213de6dbeed..000000000000 --- a/plugins/agent-workbench/lib-agent/filewatch/api-dump.txt +++ /dev/null @@ -1,37 +0,0 @@ -f:com.intellij.platform.ai.agent.filewatch.AgentWorkbenchDirectoryWatcher -- java.lang.AutoCloseable -- (java.util.Collection,kotlinx.coroutines.CoroutineScope,kotlin.jvm.functions.Function2,kotlin.jvm.functions.Function2):V -- b:(java.util.Collection,kotlinx.coroutines.CoroutineScope,kotlin.jvm.functions.Function2,kotlin.jvm.functions.Function2,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- close():V -- f:closeAndJoin(kotlin.coroutines.Continuation):java.lang.Object -- f:isActive():Z -f:com.intellij.platform.ai.agent.filewatch.AgentWorkbenchWatchEvent -- f:count:I -- f:eventType:com.intellij.platform.ai.agent.filewatch.AgentWorkbenchWatchEventType -- f:isDirectory:Z -- f:path:java.nio.file.Path -- f:rootPath:java.nio.file.Path -- (com.intellij.platform.ai.agent.filewatch.AgentWorkbenchWatchEventType,java.nio.file.Path,java.nio.file.Path,Z,I):V -- f:component1():com.intellij.platform.ai.agent.filewatch.AgentWorkbenchWatchEventType -- f:component2():java.nio.file.Path -- f:component3():java.nio.file.Path -- f:component4():Z -- f:component5():I -- f:copy(com.intellij.platform.ai.agent.filewatch.AgentWorkbenchWatchEventType,java.nio.file.Path,java.nio.file.Path,Z,I):com.intellij.platform.ai.agent.filewatch.AgentWorkbenchWatchEvent -- bs:copy$default(com.intellij.platform.ai.agent.filewatch.AgentWorkbenchWatchEvent,com.intellij.platform.ai.agent.filewatch.AgentWorkbenchWatchEventType,java.nio.file.Path,java.nio.file.Path,Z,I,I,java.lang.Object):com.intellij.platform.ai.agent.filewatch.AgentWorkbenchWatchEvent -- equals(java.lang.Object):Z -- hashCode():I -e:com.intellij.platform.ai.agent.filewatch.AgentWorkbenchWatchEventType -- java.lang.Enum -- sf:CREATE:com.intellij.platform.ai.agent.filewatch.AgentWorkbenchWatchEventType -- sf:DELETE:com.intellij.platform.ai.agent.filewatch.AgentWorkbenchWatchEventType -- sf:MODIFY:com.intellij.platform.ai.agent.filewatch.AgentWorkbenchWatchEventType -- sf:OVERFLOW:com.intellij.platform.ai.agent.filewatch.AgentWorkbenchWatchEventType -- s:getEntries():kotlin.enums.EnumEntries -- s:valueOf(java.lang.String):com.intellij.platform.ai.agent.filewatch.AgentWorkbenchWatchEventType -- s:values():com.intellij.platform.ai.agent.filewatch.AgentWorkbenchWatchEventType[] -com.intellij.platform.ai.agent.filewatch.AgentWorkbenchWatchLoop -- java.lang.AutoCloseable -- a:watch(kotlin.coroutines.Continuation):java.lang.Object -f:com.intellij.platform.ai.agent.filewatch.ImmediateFileChangeFlowKt -- sf:agentWorkbenchImmediateFileChangeFlow(java.util.Collection):kotlinx.coroutines.flow.Flow diff --git a/plugins/agent-workbench/lib-agent/filewatch/exposed-third-party-api.txt b/plugins/agent-workbench/lib-agent/filewatch/exposed-third-party-api.txt deleted file mode 100644 index 1ff3abdfef32..000000000000 --- a/plugins/agent-workbench/lib-agent/filewatch/exposed-third-party-api.txt +++ /dev/null @@ -1 +0,0 @@ -kotlin/jvm/internal/DefaultConstructorMarker diff --git a/plugins/agent-workbench/lib-agent/filewatch/intellij.platform.ai.agent.filewatch.iml b/plugins/agent-workbench/lib-agent/filewatch/intellij.platform.ai.agent.filewatch.iml deleted file mode 100644 index 3295f14679d4..000000000000 --- a/plugins/agent-workbench/lib-agent/filewatch/intellij.platform.ai.agent.filewatch.iml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/plugins/agent-workbench/lib-agent/filewatch/intellij.platform.ai.agent.filewatch.tests.iml b/plugins/agent-workbench/lib-agent/filewatch/intellij.platform.ai.agent.filewatch.tests.iml deleted file mode 100644 index 602c993deae3..000000000000 --- a/plugins/agent-workbench/lib-agent/filewatch/intellij.platform.ai.agent.filewatch.tests.iml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/plugins/agent-workbench/lib-agent/filewatch/resources/intellij.platform.ai.agent.filewatch.xml b/plugins/agent-workbench/lib-agent/filewatch/resources/intellij.platform.ai.agent.filewatch.xml deleted file mode 100644 index 6fb76a66265d..000000000000 --- a/plugins/agent-workbench/lib-agent/filewatch/resources/intellij.platform.ai.agent.filewatch.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/plugins/agent-workbench/lib-agent/filewatch/src/AgentWorkbenchDirectoryWatcher.kt b/plugins/agent-workbench/lib-agent/filewatch/src/AgentWorkbenchDirectoryWatcher.kt deleted file mode 100644 index 71510d351de9..000000000000 --- a/plugins/agent-workbench/lib-agent/filewatch/src/AgentWorkbenchDirectoryWatcher.kt +++ /dev/null @@ -1,205 +0,0 @@ -// 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.filewatch - -import com.intellij.platform.ai.agent.filewatch.impl.DirectoryChangeEvent -import com.intellij.platform.ai.agent.filewatch.impl.DirectoryChangeListener -import com.intellij.platform.ai.agent.filewatch.impl.DirectoryWatcher -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.launch -import java.nio.file.Files -import java.nio.file.Path -import java.util.concurrent.atomic.AtomicBoolean - -enum class AgentWorkbenchWatchEventType { - CREATE, - MODIFY, - DELETE, - OVERFLOW, -} - -data class AgentWorkbenchWatchEvent( - @JvmField val eventType: AgentWorkbenchWatchEventType, - @JvmField val path: Path?, - @JvmField val rootPath: Path?, - @JvmField val isDirectory: Boolean, - @JvmField val count: Int, -) - -class AgentWorkbenchDirectoryWatcher private constructor( - roots: Collection, - scope: CoroutineScope, - private val onWatchEvent: suspend (AgentWorkbenchWatchEvent) -> Unit, - private val onFailure: suspend (Throwable) -> Unit = {}, - private val watchLoopFactory: (List, DirectoryChangeListener) -> AgentWorkbenchWatchLoop, - @Suppress("unused") private val constructorMarker: Unit, -) : AutoCloseable { - private val roots = roots.toList() - private val running = AtomicBoolean(true) - @Volatile - private var watchLoop: AgentWorkbenchWatchLoop? - private val watcherJob: Job? - - constructor( - roots: Collection, - scope: CoroutineScope, - onWatchEvent: suspend (AgentWorkbenchWatchEvent) -> Unit, - onFailure: suspend (Throwable) -> Unit = {}, - ) : this(roots, scope, onWatchEvent, onFailure, ::createDefaultWatchLoop, Unit) - - internal constructor( - roots: Collection, - scope: CoroutineScope, - onWatchEvent: suspend (AgentWorkbenchWatchEvent) -> Unit, - onFailure: suspend (Throwable) -> Unit = {}, - watchLoopFactory: (List, DirectoryChangeListener) -> AgentWorkbenchWatchLoop, - ) : this(roots, scope, onWatchEvent, onFailure, watchLoopFactory, Unit) - - init { - watchLoop = createWatchLoop(roots) - watcherJob = if (watchLoop != null) { - scope.launch(Dispatchers.IO) { - runWatchLoops() - } - } - else { - null - } - } - - val isActive: Boolean - get() = watchLoop != null && watcherJob?.isActive == true - - override fun close() { - if (!running.compareAndSet(true, false)) return - watcherJob?.cancel() - watchLoop?.let { watcher -> - runCatching { - watcher.close() - } - } - } - - @Suppress("unused") - suspend fun closeAndJoin() { - if (running.compareAndSet(true, false)) { - watcherJob?.cancel() - runCatching { - watchLoop?.close() - }.onFailure { t -> - onFailure(t) - } - } - watcherJob?.cancelAndJoin() - } - - private fun createWatchLoop(roots: Collection): AgentWorkbenchWatchLoop? { - val watchRoots = LinkedHashSet() - for (root in roots) { - val normalizedPath = normalizeWatchPath(root) - if (Files.isDirectory(normalizedPath)) { - watchRoots.add(normalizedPath) - } - } - if (watchRoots.isEmpty()) { - return null - } - - val listener = object : DirectoryChangeListener { - override suspend fun onEvent(event: DirectoryChangeEvent) { - if (running.get()) { - onWatchEvent(event.toAgentWorkbenchWatchEvent()) - } - } - - override suspend fun onException(e: Exception) { - if (running.get()) { - onFailure(e) - } - } - } - return watchLoopFactory(ArrayList(watchRoots), listener) - } - - private suspend fun runWatchLoops() { - while (running.get()) { - val watcher = watchLoop ?: return - val shouldRestart = try { - watcher.watch() - false - } - catch (e: CancellationException) { - throw e - } - catch (t: Throwable) { - if (running.get()) { - onFailure(t) - true - } - else { - false - } - } - finally { - if (watchLoop === watcher) { - watchLoop = null - } - runCatching { - watcher.close() - }.onFailure { t -> - if (running.get()) { - onFailure(t) - } - } - } - - if (!shouldRestart || !running.get()) { - return - } - watchLoop = createWatchLoop(roots) - } - } -} - -interface AgentWorkbenchWatchLoop : AutoCloseable { - suspend fun watch() -} - -private fun createDefaultWatchLoop(paths: List, listener: DirectoryChangeListener): AgentWorkbenchWatchLoop { - val watcher = DirectoryWatcher(paths, listener) - return object : AgentWorkbenchWatchLoop { - override suspend fun watch() { - watcher.watch() - } - - override fun close() { - watcher.close() - } - } -} - -private fun DirectoryChangeEvent.toAgentWorkbenchWatchEvent(): AgentWorkbenchWatchEvent { - return AgentWorkbenchWatchEvent( - eventType = when (eventType) { - DirectoryChangeEvent.EventType.CREATE -> AgentWorkbenchWatchEventType.CREATE - DirectoryChangeEvent.EventType.MODIFY -> AgentWorkbenchWatchEventType.MODIFY - DirectoryChangeEvent.EventType.DELETE -> AgentWorkbenchWatchEventType.DELETE - DirectoryChangeEvent.EventType.OVERFLOW -> AgentWorkbenchWatchEventType.OVERFLOW - }, - path = path, - rootPath = rootPath, - isDirectory = isDirectory, - count = count, - ) -} - -private fun normalizeWatchPath(path: Path): Path { - return runCatching { - path.toAbsolutePath().normalize() - }.getOrElse { - path.normalize() - } -} diff --git a/plugins/agent-workbench/lib-agent/filewatch/src/ImmediateFileChangeFlow.kt b/plugins/agent-workbench/lib-agent/filewatch/src/ImmediateFileChangeFlow.kt deleted file mode 100644 index ef7787052227..000000000000 --- a/plugins/agent-workbench/lib-agent/filewatch/src/ImmediateFileChangeFlow.kt +++ /dev/null @@ -1,224 +0,0 @@ -// 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.filewatch - -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.jna.KEvent -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.jna.K_EVENT_ADD -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.jna.K_EVENT_CLEAR -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.jna.K_EVENT_FILTER_VNODE -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.jna.K_NOTE_DELETE -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.jna.K_NOTE_EXTEND -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.jna.K_NOTE_RENAME -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.jna.K_NOTE_REVOKE -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.jna.K_NOTE_WRITE -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.jna.K_QUEUE_OPEN_EVENT_ONLY -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.jna.macKQueueApi -import com.intellij.openapi.util.SystemInfoRt.isMac -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.emptyFlow -import java.nio.file.Files -import java.nio.file.Path - -fun agentWorkbenchImmediateFileChangeFlow(paths: Collection): Flow { - if (!isMac || paths.isEmpty()) { - return emptyFlow() - } - val normalizedPaths = paths.asSequence() - .map(::normalizeImmediateWatchPath) - .distinct() - .toList() - if (normalizedPaths.isEmpty()) { - return emptyFlow() - } - - return callbackFlow { - val watcher = MacOSXImmediateFileChangeWatcher.create( - paths = normalizedPaths, - onModify = { path -> trySend(path).isSuccess }, - onEmpty = { close() }, - ) - if (watcher == null) { - close() - return@callbackFlow - } - awaitClose { - watcher.close() - } - } -} - -private class MacOSXImmediateFileChangeWatcher private constructor( - private val kqueueFd: Int, - private val onModify: (Path) -> Unit, - private val onEmpty: () -> Unit, -) : AutoCloseable { - companion object { - fun create(paths: Collection, onModify: (Path) -> Unit, onEmpty: () -> Unit): MacOSXImmediateFileChangeWatcher? { - val kqueueFd = runCatching { - macKQueueApi.kqueue() - }.getOrDefault(-1) - if (kqueueFd < 0) { - return null - } - - val watcher = MacOSXImmediateFileChangeWatcher(kqueueFd, onModify, onEmpty) - for (path in paths) { - watcher.watch(path) - } - if (!watcher.hasWatchedFiles()) { - watcher.close() - return null - } - watcher.start() - return watcher - } - } - - private val lock = Any() - private val watchedFiles = LinkedHashMap() - private val watchedFds = HashMap() - @Volatile - private var closed = false - - private fun start() { - Thread({ runEventLoop() }, "Agent Workbench macOS immediate file watcher").apply { - isDaemon = true - start() - } - } - - private fun hasWatchedFiles(): Boolean { - return synchronized(lock) { - watchedFiles.isNotEmpty() - } - } - - private fun watch(path: Path) { - if (!Files.isRegularFile(path)) { - return - } - synchronized(lock) { - if (closed || path in watchedFiles) { - return - } - } - - val fd = runCatching { - macKQueueApi.open(path.toString(), K_QUEUE_OPEN_EVENT_ONLY) - }.getOrDefault(-1) - if (fd < 0) { - return - } - - var registered = false - try { - val event = KEvent( - ident = fd, - filter = K_EVENT_FILTER_VNODE, - flags = (K_EVENT_ADD.toInt() or K_EVENT_CLEAR.toInt()).toShort(), - fflags = K_NOTE_WRITE or K_NOTE_EXTEND or K_NOTE_DELETE or K_NOTE_RENAME or K_NOTE_REVOKE, - ) - val result = runCatching { - macKQueueApi.kevent(kqueueFd, event, 1, null, 0, null) - }.getOrDefault(-1) - if (result < 0) { - return - } - val watchedFile = WatchedFile(path, fd) - synchronized(lock) { - if (!closed && path !in watchedFiles) { - watchedFiles[path] = watchedFile - watchedFds[fd] = watchedFile - registered = true - } - } - } - finally { - if (!registered) { - closeFileDescriptor(fd) - } - } - } - - private fun unwatch(path: Path) { - removeWatch(path)?.let { watchedFile -> - closeFileDescriptor(watchedFile.fd) - } - } - - override fun close() { - val watchedFilesToClose = synchronized(lock) { - if (closed) { - return - } - closed = true - val files = ArrayList(watchedFiles.values) - watchedFiles.clear() - watchedFds.clear() - files - } - closeFileDescriptor(kqueueFd) - for ((_, fd) in watchedFilesToClose) { - closeFileDescriptor(fd) - } - } - - private fun runEventLoop() { - while (!closed) { - val event = KEvent() - val result = runCatching { - macKQueueApi.kevent(kqueueFd, null, 0, event, 1, null) - }.getOrDefault(-1) - if (result < 0) { - return - } - if (result == 0) { - continue - } - handleEvent(event) - } - } - - private fun handleEvent(event: KEvent) { - val fd = event.ident.toInt() - val watchedFile = synchronized(lock) { - watchedFds[fd] - } ?: return - - val flags = event.fflags - if (flags and (K_NOTE_WRITE or K_NOTE_EXTEND) != 0) { - onModify(watchedFile.path) - } - if (flags and (K_NOTE_DELETE or K_NOTE_RENAME or K_NOTE_REVOKE) != 0) { - unwatch(watchedFile.path) - if (!hasWatchedFiles()) { - onEmpty() - } - } - } - - private fun removeWatch(path: Path): WatchedFile? { - return synchronized(lock) { - val watchedFile = watchedFiles.remove(path) ?: return@synchronized null - watchedFds.remove(watchedFile.fd) - watchedFile - } - } - - private fun closeFileDescriptor(fd: Int) { - runCatching { - macKQueueApi.close(fd) - } - } - - private data class WatchedFile(@JvmField val path: Path, @JvmField val fd: Int) -} - -private fun normalizeImmediateWatchPath(path: Path): Path { - return runCatching { - path.toAbsolutePath().normalize() - }.getOrElse { - path.normalize() - } -} diff --git a/plugins/agent-workbench/lib-agent/filewatch/src/impl/DirectoryChangeEvent.kt b/plugins/agent-workbench/lib-agent/filewatch/src/impl/DirectoryChangeEvent.kt deleted file mode 100644 index b2ff2b8cc5a7..000000000000 --- a/plugins/agent-workbench/lib-agent/filewatch/src/impl/DirectoryChangeEvent.kt +++ /dev/null @@ -1,19 +0,0 @@ -// 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.filewatch.impl - -import java.nio.file.Path - -internal data class DirectoryChangeEvent( - val eventType: EventType, - val isDirectory: Boolean, - val path: Path?, - val count: Int, - val rootPath: Path?, -) { - internal enum class EventType { - CREATE, - MODIFY, - DELETE, - OVERFLOW, - } -} diff --git a/plugins/agent-workbench/lib-agent/filewatch/src/impl/DirectoryChangeListener.kt b/plugins/agent-workbench/lib-agent/filewatch/src/impl/DirectoryChangeListener.kt deleted file mode 100644 index 70223eb8af82..000000000000 --- a/plugins/agent-workbench/lib-agent/filewatch/src/impl/DirectoryChangeListener.kt +++ /dev/null @@ -1,9 +0,0 @@ -// 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.filewatch.impl - -internal fun interface DirectoryChangeListener { - suspend fun onEvent(event: DirectoryChangeEvent) - - suspend fun onException(e: Exception) { - } -} diff --git a/plugins/agent-workbench/lib-agent/filewatch/src/impl/DirectoryWatcher.kt b/plugins/agent-workbench/lib-agent/filewatch/src/impl/DirectoryWatcher.kt deleted file mode 100644 index 08982e7aac30..000000000000 --- a/plugins/agent-workbench/lib-agent/filewatch/src/impl/DirectoryWatcher.kt +++ /dev/null @@ -1,211 +0,0 @@ -// 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.filewatch.impl - -import com.intellij.platform.ai.agent.filewatch.impl.DirectoryChangeEvent.EventType -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.MacOSXListeningWatchService -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.WatchablePath -import com.intellij.openapi.diagnostic.debug -import com.intellij.openapi.diagnostic.logger -import com.sun.nio.file.ExtendedWatchEventModifier -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.isActive -import kotlinx.coroutines.runInterruptible -import java.nio.file.ClosedWatchServiceException -import java.nio.file.FileSystems -import java.nio.file.Files -import java.nio.file.LinkOption.NOFOLLOW_LINKS -import java.nio.file.Path -import java.nio.file.StandardWatchEventKinds.ENTRY_CREATE -import java.nio.file.StandardWatchEventKinds.ENTRY_DELETE -import java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY -import java.nio.file.StandardWatchEventKinds.OVERFLOW -import java.nio.file.WatchEvent -import java.nio.file.WatchKey -import java.nio.file.WatchService -import java.nio.file.Watchable -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicBoolean - -private val LOG = logger() - -internal class DirectoryWatcher( - paths: List, - private val listener: DirectoryChangeListener, - private val watchService: WatchService = createDefaultWatchService(), - private val fileTreeVisitor: FileTreeVisitor = DefaultFileTreeVisitor, - private val watchablePathFactory: (Path) -> Watchable = if (watchService is MacOSXListeningWatchService) ::WatchablePath else { path -> path }, -) : AutoCloseable { - private val paths = paths.map { path -> path.toAbsolutePath() } - private val isMac = watchService is MacOSXListeningWatchService - private val registeredPathToRootPath = HashMap() - private val directories = ConcurrentHashMap.newKeySet() - private val keyRoots = ConcurrentHashMap() - private val closed = AtomicBoolean() - private var fileTreeSupported: Boolean? = null - - suspend fun watch() { - check(!closed.get()) { "watcher already closed" } - use { - registerPaths() - runEventLoop() - } - } - - override fun close() { - if (closed.compareAndSet(false, true)) { - watchService.close() - } - } - - private suspend fun registerPaths() { - for (path in paths) { - registerAll(path, path) - } - } - - private suspend fun runEventLoop() { - while (!closed.get() && currentCoroutineContext().isActive) { - val key = try { - runInterruptible { watchService.take() } - } - catch (_: InterruptedException) { - return - } - catch (_: ClosedWatchServiceException) { - return - } - for (event in key.pollEvents()) { - try { - handleEvent(key, event) - } - catch (e: Exception) { - LOG.debug("DirectoryWatcher got an exception while watching!", e) - listener.onException(e) - } - } - if (!key.reset()) { - LOG.debug { "WatchKey for [${key.watchable()}] no longer valid; removing." } - val registeredPath = keyRoots.remove(key) - registeredPathToRootPath.remove(registeredPath) - if (keyRoots.isEmpty()) { - LOG.debug("No more directories left to watch; terminating watcher.") - throw IllegalStateException("No more directories left to watch") - } - } - } - } - - private suspend fun handleEvent(key: WatchKey, event: WatchEvent<*>) { - val kind = event.kind() - val count = event.count() - val eventPath = event.pathContext() - val registeredPath = keyRoots[key] ?: throw IllegalStateException("WatchService returned key [$key] but it was not found in keyRoots!") - val rootPath = registeredPathToRootPath[registeredPath] - val childPath = eventPath?.let { registeredPath.resolve(it) } - LOG.debug { "$kind [$childPath]" } - when { - kind === OVERFLOW -> onEvent(EventType.OVERFLOW, false, childPath, count, rootPath) - eventPath == null -> throw IllegalStateException("WatchService returned a null path for ${kind.name()}") - kind === ENTRY_CREATE -> handleCreate(childPath!!, count, rootPath) - kind === ENTRY_MODIFY -> onEvent(EventType.MODIFY, directories.contains(childPath), childPath, count, rootPath) - kind === ENTRY_DELETE -> onEvent(EventType.DELETE, directories.remove(childPath), childPath, count, rootPath) - } - } - - private suspend fun handleCreate(childPath: Path, count: Int, rootPath: Path?) { - val isDirectory = Files.isDirectory(childPath, NOFOLLOW_LINKS) - if (isDirectory) { - if (!isFileTreeSupported()) { - registerAll(childPath, rootPath) - } - if (!isMac) { - val createdDirectories = ArrayList() - val createdFiles = ArrayList() - runInterruptible { - fileTreeVisitor.recursiveVisitFiles(childPath, createdDirectories::add, createdFiles::add) - } - for (dir in createdDirectories) { - notifyCreateEvent(true, dir, count, rootPath) - } - for (file in createdFiles) { - notifyCreateEvent(false, file, count, rootPath) - } - } - else { - notifyCreateEvent(true, childPath, count, rootPath) - } - } - else { - notifyCreateEvent(false, childPath, count, rootPath) - } - } - - private suspend fun onEvent(eventType: EventType, isDirectory: Boolean, childPath: Path?, count: Int, rootPath: Path?) { - LOG.debug { "-> $eventType [$childPath] (isDirectory: $isDirectory)" } - listener.onEvent(DirectoryChangeEvent(eventType, isDirectory, childPath, count, rootPath)) - } - - private suspend fun registerAll(start: Path, context: Path?) { - if (fileTreeSupported != false) { - try { - register(start, true, context) - fileTreeSupported = true - addKnownDirectories(start) - } - catch (e: UnsupportedOperationException) { - LOG.debug("Assuming ExtendedWatchEventModifier.FILE_TREE is not supported", e) - fileTreeSupported = false - registerAll(start, context) - } - } - else { - val directories = ArrayList() - runInterruptible { - fileTreeVisitor.recursiveVisitFiles(start, directories::add) {} - } - for (directory in directories) { - this.directories.add(directory) - register(directory, false, context) - } - } - } - - private suspend fun addKnownDirectories(start: Path) { - runInterruptible { - fileTreeVisitor.recursiveVisitFiles(start, directories::add) {} - } - } - - private suspend fun register(directory: Path, useFileTreeModifier: Boolean, context: Path?) { - LOG.debug { "Registering [$directory]." } - val modifiers = if (useFileTreeModifier) arrayOf(ExtendedWatchEventModifier.FILE_TREE) else emptyArray() - val watchKey = runInterruptible { - val watchable = watchablePathFactory(directory) - watchable.register(watchService, arrayOf(ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY), *modifiers) - } - keyRoots[watchKey] = directory - if (context != null) { - registeredPathToRootPath[directory] = context - } - } - - private suspend fun notifyCreateEvent(isDirectory: Boolean, path: Path, count: Int, rootPath: Path?) { - if (isDirectory) { - directories.add(path) - } - onEvent(EventType.CREATE, isDirectory, path, count, rootPath) - } - - private fun isFileTreeSupported(): Boolean = fileTreeSupported ?: error("fileTreeSupported not initialized") - -} - -private fun createDefaultWatchService(): WatchService { - return if (System.getProperty("os.name").contains("mac", ignoreCase = true)) { - MacOSXListeningWatchService(object : MacOSXListeningWatchService.Config { - }) - } - else { - FileSystems.getDefault().newWatchService() - } -} diff --git a/plugins/agent-workbench/lib-agent/filewatch/src/impl/FileTreeVisitor.kt b/plugins/agent-workbench/lib-agent/filewatch/src/impl/FileTreeVisitor.kt deleted file mode 100644 index 81c793e6aa1b..000000000000 --- a/plugins/agent-workbench/lib-agent/filewatch/src/impl/FileTreeVisitor.kt +++ /dev/null @@ -1,28 +0,0 @@ -// 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.filewatch.impl - -import java.nio.file.FileVisitResult -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.SimpleFileVisitor -import java.nio.file.attribute.BasicFileAttributes - -internal interface FileTreeVisitor { - fun recursiveVisitFiles(root: Path, directoryConsumer: (Path) -> Unit, fileConsumer: (Path) -> Unit) -} - -internal object DefaultFileTreeVisitor : FileTreeVisitor { - override fun recursiveVisitFiles(root: Path, directoryConsumer: (Path) -> Unit, fileConsumer: (Path) -> Unit) { - Files.walkFileTree(root, object : SimpleFileVisitor() { - override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult { - directoryConsumer(dir) - return FileVisitResult.CONTINUE - } - - override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { - fileConsumer(file) - return FileVisitResult.CONTINUE - } - }) - } -} diff --git a/plugins/agent-workbench/lib-agent/filewatch/src/impl/PathUtils.kt b/plugins/agent-workbench/lib-agent/filewatch/src/impl/PathUtils.kt deleted file mode 100644 index d01e77697dbe..000000000000 --- a/plugins/agent-workbench/lib-agent/filewatch/src/impl/PathUtils.kt +++ /dev/null @@ -1,18 +0,0 @@ -// 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.filewatch.impl - -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.WatchEvent - -@Suppress("UNCHECKED_CAST") -internal fun WatchEvent<*>.pathContext(): Path? = (this as WatchEvent).context() - -internal fun recursiveListFiles(fileTreeVisitor: FileTreeVisitor, root: Path): Set { - val files = HashSet() - when { - Files.isDirectory(root) -> fileTreeVisitor.recursiveVisitFiles(root, files::add, files::add) - Files.exists(root) -> files.add(root) - } - return files -} diff --git a/plugins/agent-workbench/lib-agent/filewatch/src/impl/watchservice/AbstractWatchService.kt b/plugins/agent-workbench/lib-agent/filewatch/src/impl/watchservice/AbstractWatchService.kt deleted file mode 100644 index 38a4cb882242..000000000000 --- a/plugins/agent-workbench/lib-agent/filewatch/src/impl/watchservice/AbstractWatchService.kt +++ /dev/null @@ -1,177 +0,0 @@ -// 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.filewatch.impl.watchservice - -import java.nio.file.ClosedWatchServiceException -import java.nio.file.Path -import java.nio.file.ProviderMismatchException -import java.nio.file.StandardWatchEventKinds.OVERFLOW -import java.nio.file.WatchEvent -import java.nio.file.WatchKey -import java.nio.file.WatchService -import java.nio.file.Watchable -import java.util.Collections -import java.util.concurrent.ArrayBlockingQueue -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicInteger -import java.util.concurrent.atomic.AtomicReference - -internal abstract class AbstractWatchService : WatchService { - private val queue = LinkedBlockingQueue() - private val poison = AbstractWatchKey(this, null, emptySet(), 1) - private val open = AtomicBoolean(true) - - abstract fun register(watchable: WatchablePath, eventTypes: Iterable>): AbstractWatchKey - - val isOpen: Boolean - get() = open.get() - - internal fun enqueue(key: AbstractWatchKey) { - if (isOpen) { - queue.add(key) - } - } - - open fun cancelled(key: AbstractWatchKey) { - } - - override fun poll(): WatchKey? { - checkOpen() - return check(queue.poll()) - } - - override fun poll(timeout: Long, unit: TimeUnit): WatchKey? { - checkOpen() - return check(queue.poll(timeout, unit)) - } - - override fun take(): WatchKey { - checkOpen() - return check(queue.take()) ?: error("queue.take() returned null") - } - - private fun check(key: WatchKey?): WatchKey? { - if (key === poison) { - queue.offer(poison) - throw ClosedWatchServiceException() - } - return key - } - - protected fun checkOpen() { - if (!open.get()) { - throw ClosedWatchServiceException() - } - } - - override fun close() { - if (open.compareAndSet(true, false)) { - queue.clear() - queue.offer(poison) - } - } - - internal data class Event( - private val kind: WatchEvent.Kind, - private val count: Int, - private val context: T?, - ) : WatchEvent { - init { - require(count >= 0) { "count ($count) must be non-negative" } - } - - override fun kind(): WatchEvent.Kind = kind - - override fun count(): Int = count - - override fun context(): T? = context - } -} - -internal open class AbstractWatchKey( - private val watcher: AbstractWatchService, - private val watchable: Watchable?, - @Suppress("unused") subscribedTypes: Iterable>, - queueSize: Int, -) : WatchKey { - private val state = AtomicReference(State.READY) - private val valid = AtomicBoolean(true) - private val overflow = AtomicInteger() - private val events = ArrayBlockingQueue>(queueSize) - - fun post(event: WatchEvent<*>) { - if (!events.offer(event)) { - overflow.incrementAndGet() - } - } - - fun signal() { - if (state.getAndSet(State.SIGNALLED) == State.READY) { - watcher.enqueue(this) - } - } - - override fun isValid(): Boolean = watcher.isOpen && valid.get() - - override fun pollEvents(): List> { - val result = ArrayList>(events.size) - events.drainTo(result) - val overflowCount = overflow.getAndSet(0) - if (overflowCount != 0) { - result.add(AbstractWatchService.Event(OVERFLOW, overflowCount, null)) - } - return Collections.unmodifiableList(result) - } - - override fun reset(): Boolean { - if (isValid && state.compareAndSet(State.SIGNALLED, State.READY) && hasPendingEvents()) { - signal() - } - return isValid - } - - private fun hasPendingEvents(): Boolean = !events.isEmpty() || overflow.get() != 0 - - override fun cancel() { - valid.set(false) - watcher.cancelled(this) - } - - override fun watchable(): Watchable? = watchable - - fun signalEvent(kind: WatchEvent.Kind, context: Path) { - post(AbstractWatchService.Event(kind, 1, context)) - signal() - } - - fun signalOverflow(context: Path?) { - post(AbstractWatchService.Event(OVERFLOW, 1, context)) - signal() - } - - override fun toString(): String = "AbstractWatchKey{watchable=$watchable, valid=${valid.get()}}" - - private enum class State { - READY, - SIGNALLED, - } -} - -internal class WatchablePath(val file: Path) : Watchable { - override fun register(watcher: WatchService, events: Array>, vararg modifiers: WatchEvent.Modifier): WatchKey { - if (watcher !is AbstractWatchService) { - throw ProviderMismatchException() - } - return watcher.register(this, events.asList()) - } - - override fun register(watcher: WatchService, vararg events: WatchEvent.Kind<*>): WatchKey { - if (!java.nio.file.Files.exists(file)) { - throw RuntimeException("Directory to watch doesn't exist: $file") - } - return register(watcher, events) - } - - override fun toString(): String = "Path{file=$file}" -} diff --git a/plugins/agent-workbench/lib-agent/filewatch/src/impl/watchservice/MacOSXListeningWatchService.kt b/plugins/agent-workbench/lib-agent/filewatch/src/impl/watchservice/MacOSXListeningWatchService.kt deleted file mode 100644 index efcac455c524..000000000000 --- a/plugins/agent-workbench/lib-agent/filewatch/src/impl/watchservice/MacOSXListeningWatchService.kt +++ /dev/null @@ -1,364 +0,0 @@ -// 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.filewatch.impl.watchservice - -import com.intellij.platform.ai.agent.filewatch.impl.DefaultFileTreeVisitor -import com.intellij.platform.ai.agent.filewatch.impl.FileTreeVisitor -import com.intellij.platform.ai.agent.filewatch.impl.recursiveListFiles -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.jna.CFArrayRef -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.jna.FSEventStreamRef -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.jna.K_FSEVENT_STREAM_CREATE_FLAG_FILE_EVENTS -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.jna.K_FSEVENT_STREAM_CREATE_FLAG_NO_DEFER -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.jna.MacFSEventsApi -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.jna.cfIndex -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.jna.libDispatchApi -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.jna.macFSEventsApi -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.jna.toCFString -import com.sun.jna.NativeLong -import com.sun.jna.Pointer -import java.nio.file.Files -import java.nio.file.NoSuchFileException -import java.nio.file.Path -import java.nio.file.StandardWatchEventKinds.ENTRY_CREATE -import java.nio.file.StandardWatchEventKinds.ENTRY_DELETE -import java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY -import java.nio.file.WatchEvent - -internal class MacOSXListeningWatchService(config: Config) : AbstractWatchService() { - interface Config { - val latency: Double - get() = DEFAULT_LATENCY - val queueSize: Int - get() = DEFAULT_QUEUE_SIZE - val fileTreeVisitor: FileTreeVisitor - get() = DefaultFileTreeVisitor - } - - private val callbacks = ArrayList() - private val streams = ArrayList() - private val pathsWatching = HashSet() - private val latency = config.latency - private val queueSize = config.queueSize - private val fileTreeVisitor = config.fileTreeVisitor - - @Synchronized - override fun register(watchable: WatchablePath, eventTypes: Iterable>): AbstractWatchKey { - checkOpen() - val watchKey = MacOSXWatchKey(this, watchable, eventTypes, queueSize) - val file = watchable.file.toAbsolutePath() - val pathToWatch = file.toString().toCFString() - val values = arrayOf(pathToWatch.pointer) - val pathsToWatch: CFArrayRef = macFSEventsApi.CFArrayCreate(null, values, cfIndex(1), null) - val callback = MacOSXListeningCallback(watchKey, fileTreeVisitor, file) - try { - val streamRef = macFSEventsApi.FSEventStreamCreate( - Pointer.NULL, - callback, - Pointer.NULL, - pathsToWatch, - -1L, - latency, - K_FSEVENT_STREAM_CREATE_FLAG_NO_DEFER or K_FSEVENT_STREAM_CREATE_FLAG_FILE_EVENTS, - ) - val stream = FSEventStream(streamRef, file) - callback.onClose { close(stream, callback, file) } - stream.start() - callbacks.add(callback) - streams.add(stream) - pathsWatching.add(file) - return watchKey - } - catch (e: Exception) { - watchKey.cancel() - throw e - } - finally { - macFSEventsApi.CFRelease(pathsToWatch) - macFSEventsApi.CFRelease(pathToWatch) - } - } - - @Synchronized - override fun close() { - super.close() - streams.forEach(FSEventStream::close) - streams.clear() - callbacks.clear() - pathsWatching.clear() - } - - @Synchronized - private fun close(stream: FSEventStream, callback: MacOSXListeningCallback, path: Path) { - streams.remove(stream) - callbacks.remove(callback) - pathsWatching.remove(path) - stream.close() - } - - internal class FSEventStream( - private val streamRef: FSEventStreamRef, - private val file: Path, - ) : AutoCloseable { - private val dispatchQueue = libDispatchApi.dispatch_queue_create("WatchService for $file", null) - private var closed = false - - fun start() { - synchronized(streamRef) { - if (closed) { - return - } - macFSEventsApi.FSEventStreamSetDispatchQueue(streamRef, dispatchQueue) - if (!macFSEventsApi.FSEventStreamStart(streamRef)) { - close() - throw IllegalStateException("Could not start FSEventStream for $file") - } - } - } - - override fun close() { - synchronized(streamRef) { - if (closed) { - return - } - macFSEventsApi.FSEventStreamStop(streamRef) - macFSEventsApi.FSEventStreamInvalidate(streamRef) - macFSEventsApi.FSEventStreamRelease(streamRef) - libDispatchApi.dispatch_release(dispatchQueue) - closed = true - } - } - } -} - -private class MacOSXListeningCallback( - private val watchKey: MacOSXWatchKey, - private val fileTreeVisitor: FileTreeVisitor, - private val absPath: Path, -) : MacFSEventsApi.FSEventStreamCallback { - private val realPathSize: Int - private val knownFiles: MutableSet - private var onCloseCallback: (() -> Unit)? = null - - init { - try { - val realPath = absPath.toRealPath() - knownFiles = HashSet(recursiveListFiles(fileTreeVisitor, absPath)) - realPathSize = realPath.toString().length + 1 - } - catch (e: Exception) { - throw IllegalStateException("Could not initialize watcher for $absPath", e) - } - } - - fun onClose(callback: () -> Unit) { - onCloseCallback = callback - } - - override fun invoke( - streamRef: FSEventStreamRef, - clientCallBackInfo: Pointer?, - numEvents: NativeLong, - eventPaths: Pointer, - eventFlags: Pointer?, - eventIds: Pointer?, - ) { - val length = numEvents.toInt() - val pathNames = eventPaths.getStringArray(0, length) - for (index in pathNames.indices) { - if (!watchKey.isValid) { - return - } - val path = toWatchedPath(pathNames[index]) - val flags = FSEventFlags(eventFlags?.getInt((index * Int.SIZE_BYTES).toLong()) ?: 0) - when { - flags.requiresRescan -> rescanAfterOverflow(path) - flags.removed -> handleRemoved(path) - flags.created -> handleCreated(path, flags) - flags.renamed -> handleRenamed(path, flags) - flags.modified -> handleModified(path) - path in knownFiles && Files.exists(path) -> handleModified(path) - else -> rescanPath(path) - } - } - } - - private fun toWatchedPath(fileName: String): Path { - return if (fileName.length + 1 != realPathSize) absPath.resolve(fileName.substring(realPathSize)) else absPath - } - - private fun handleCreated(path: Path, flags: FSEventFlags) { - val wasKnown = !knownFiles.add(path) - if (wasKnown) { - handleModified(path) - } - else if (watchKey.reportCreateEvents) { - watchKey.signalEvent(ENTRY_CREATE, path) - } - if (flags.directory) { - rescanPath(path) - } - } - - private fun handleRemoved(path: Path) { - val deletedPaths = removeKnownSubtree(path) - if (deletedPaths.isEmpty()) { - return - } - for (deletedPath in deletedPaths) { - if (watchKey.reportDeleteEvents) { - watchKey.signalEvent(ENTRY_DELETE, deletedPath) - } - } - closeIfRootRemoved() - } - - private fun handleRenamed(path: Path, flags: FSEventFlags) { - if (Files.exists(path)) { - if (path !in knownFiles) { - handleCreated(path, flags) - } - else { - handleModified(path) - } - } - else { - handleRemoved(path) - } - } - - private fun handleModified(path: Path) { - if (path in knownFiles && !Files.isDirectory(path) && watchKey.reportModifyEvents) { - watchKey.signalEvent(ENTRY_MODIFY, path) - } - } - - private fun rescanAfterOverflow(path: Path) { - watchKey.signalOverflow(path) - rescanPath(absPath) - } - - private fun rescanPath(path: Path) { - val previouslyKnownFiles = HashSet(knownFiles) - val filesOnDisk = try { - recursiveListFiles(fileTreeVisitor, path) - } - catch (_: NoSuchFileException) { - handleRemoved(path) - return - } - catch (_: Exception) { - watchKey.signalOverflow(path) - return - } - for (file in findCreatedFiles(filesOnDisk)) { - if (watchKey.reportCreateEvents) { - watchKey.signalEvent(ENTRY_CREATE, file) - } - } - for (file in findModifiedFiles(filesOnDisk, previouslyKnownFiles)) { - if (watchKey.reportModifyEvents) { - watchKey.signalEvent(ENTRY_MODIFY, file) - } - } - for (file in findDeletedFiles(path, filesOnDisk)) { - if (watchKey.reportDeleteEvents) { - watchKey.signalEvent(ENTRY_DELETE, file) - } - } - closeIfRootRemoved() - } - - private fun removeKnownSubtree(path: Path): List { - val deletedPaths = ArrayList() - for (file in ArrayList(knownFiles)) { - if (file.startsWith(path)) { - deletedPaths.add(file) - knownFiles.remove(file) - } - } - return deletedPaths - } - - private fun closeIfRootRemoved() { - if (knownFiles.isEmpty()) { - watchKey.cancel() - onCloseCallback?.invoke() - } - } - - private fun findCreatedFiles(filesOnDisk: Set): List { - val createdFiles = ArrayList() - for (file in filesOnDisk) { - if (knownFiles.add(file)) { - createdFiles.add(file) - } - } - return createdFiles - } - - private fun findModifiedFiles(filesOnDisk: Set, previouslyKnownFiles: Set): List { - val modifiedFiles = ArrayList() - for (file in filesOnDisk) { - if (file in previouslyKnownFiles && file in knownFiles && !Files.isDirectory(file)) { - modifiedFiles.add(file) - } - } - return modifiedFiles - } - - private fun findDeletedFiles(path: Path, filesOnDisk: Set): List { - val deletedFiles = ArrayList() - for (file in ArrayList(knownFiles)) { - if (file.startsWith(path) && file !in filesOnDisk) { - deletedFiles.add(file) - knownFiles.remove(file) - } - } - return deletedFiles - } -} - -private class FSEventFlags(private val value: Int) { - val created: Boolean = has(K_FSEVENT_STREAM_EVENT_FLAG_ITEM_CREATED) - val removed: Boolean = has(K_FSEVENT_STREAM_EVENT_FLAG_ITEM_REMOVED) - val renamed: Boolean = has(K_FSEVENT_STREAM_EVENT_FLAG_ITEM_RENAMED) - val directory: Boolean = has(K_FSEVENT_STREAM_EVENT_FLAG_ITEM_IS_DIR) - val modified: Boolean = has(K_FSEVENT_STREAM_EVENT_FLAG_ITEM_MODIFIED) || - has(K_FSEVENT_STREAM_EVENT_FLAG_ITEM_INODE_META_MOD) || - has(K_FSEVENT_STREAM_EVENT_FLAG_ITEM_FINDER_INFO_MOD) || - has(K_FSEVENT_STREAM_EVENT_FLAG_ITEM_CHANGE_OWNER) || - has(K_FSEVENT_STREAM_EVENT_FLAG_ITEM_XATTR_MOD) - val requiresRescan: Boolean = has(K_FSEVENT_STREAM_EVENT_FLAG_MUST_SCAN_SUB_DIRS) || - has(K_FSEVENT_STREAM_EVENT_FLAG_USER_DROPPED) || - has(K_FSEVENT_STREAM_EVENT_FLAG_KERNEL_DROPPED) || - has(K_FSEVENT_STREAM_EVENT_FLAG_ROOT_CHANGED) - - private fun has(flag: Int): Boolean = value and flag != 0 -} - -private const val K_FSEVENT_STREAM_EVENT_FLAG_MUST_SCAN_SUB_DIRS: Int = 0x00000001 -private const val K_FSEVENT_STREAM_EVENT_FLAG_USER_DROPPED: Int = 0x00000002 -private const val K_FSEVENT_STREAM_EVENT_FLAG_KERNEL_DROPPED: Int = 0x00000004 -private const val K_FSEVENT_STREAM_EVENT_FLAG_ROOT_CHANGED: Int = 0x00000020 -private const val K_FSEVENT_STREAM_EVENT_FLAG_ITEM_CREATED: Int = 0x00000100 -private const val K_FSEVENT_STREAM_EVENT_FLAG_ITEM_REMOVED: Int = 0x00000200 -private const val K_FSEVENT_STREAM_EVENT_FLAG_ITEM_INODE_META_MOD: Int = 0x00000400 -private const val K_FSEVENT_STREAM_EVENT_FLAG_ITEM_RENAMED: Int = 0x00000800 -private const val K_FSEVENT_STREAM_EVENT_FLAG_ITEM_MODIFIED: Int = 0x00001000 -private const val K_FSEVENT_STREAM_EVENT_FLAG_ITEM_FINDER_INFO_MOD: Int = 0x00002000 -private const val K_FSEVENT_STREAM_EVENT_FLAG_ITEM_CHANGE_OWNER: Int = 0x00004000 -private const val K_FSEVENT_STREAM_EVENT_FLAG_ITEM_XATTR_MOD: Int = 0x00008000 -private const val K_FSEVENT_STREAM_EVENT_FLAG_ITEM_IS_DIR: Int = 0x00020000 - -private class MacOSXWatchKey( - macOSXWatchService: AbstractWatchService, - watchable: WatchablePath, - events: Iterable>, - queueSize: Int, -) : AbstractWatchKey(macOSXWatchService, watchable, events, queueSize) { - val reportCreateEvents = events.any { it === ENTRY_CREATE } - val reportModifyEvents = events.any { it === ENTRY_MODIFY } - val reportDeleteEvents = events.any { it === ENTRY_DELETE } -} - -private const val DEFAULT_LATENCY = 0.5 -private const val DEFAULT_QUEUE_SIZE = 1024 diff --git a/plugins/agent-workbench/lib-agent/filewatch/src/impl/watchservice/jna/MacFSEventsApi.kt b/plugins/agent-workbench/lib-agent/filewatch/src/impl/watchservice/jna/MacFSEventsApi.kt deleted file mode 100644 index f7d2610ea47a..000000000000 --- a/plugins/agent-workbench/lib-agent/filewatch/src/impl/watchservice/jna/MacFSEventsApi.kt +++ /dev/null @@ -1,90 +0,0 @@ -// 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.filewatch.impl.watchservice.jna - -import com.sun.jna.Callback -import com.sun.jna.Library -import com.sun.jna.Native -import com.sun.jna.NativeLong -import com.sun.jna.Pointer -import com.sun.jna.ptr.PointerByReference - -@Suppress("FunctionName") -internal interface MacFSEventsApi : Library { - interface FSEventStreamCallback : Callback { - fun invoke( - streamRef: FSEventStreamRef, - clientCallBackInfo: Pointer?, - numEvents: NativeLong, - eventPaths: Pointer, - eventFlags: Pointer?, - eventIds: Pointer?, - ) - } - - fun CFArrayCreate(allocator: CFAllocatorRef?, values: Array, numValues: CFIndex, callbacks: Void?): CFArrayRef - - fun CFStringCreateWithCharacters(alloc: Void?, chars: CharArray, numChars: CFIndex): CFStringRef - - fun FSEventStreamCreate( - allocator: Pointer?, - callback: FSEventStreamCallback, - context: Pointer?, - pathsToWatch: CFArrayRef, - sinceWhen: Long, - latency: Double, - flags: Int, - ): FSEventStreamRef - - fun FSEventStreamSetDispatchQueue(streamRef: FSEventStreamRef, dispatchQueue: DispatchQueueRef) - - fun FSEventStreamStart(streamRef: FSEventStreamRef): Boolean - - fun FSEventStreamStop(streamRef: FSEventStreamRef) - - fun FSEventStreamInvalidate(streamRef: FSEventStreamRef) - - fun FSEventStreamRelease(streamRef: FSEventStreamRef) - - fun CFRelease(ref: PointerByReference) -} - -internal val macFSEventsApi: MacFSEventsApi = Native.load("CoreServices", MacFSEventsApi::class.java) - -internal const val K_FSEVENT_STREAM_CREATE_FLAG_NO_DEFER: Int = 0x00000002 -internal const val K_FSEVENT_STREAM_CREATE_FLAG_FILE_EVENTS: Int = 0x00000010 - -@Suppress("FunctionName") -internal interface LibDispatchApi : Library { - fun dispatch_queue_create(label: String, attr: Pointer?): DispatchQueueRef - - fun dispatch_release(dispatchObject: DispatchQueueRef) -} - -internal val libDispatchApi: LibDispatchApi = Native.load("System", LibDispatchApi::class.java) - -internal class CFAllocatorRef : PointerByReference() - -internal class CFArrayRef : PointerByReference() - -internal class FSEventStreamRef : PointerByReference() - -internal class DispatchQueueRef : PointerByReference() - -internal class CFIndex() : NativeLong() { - constructor(value: Long) : this() { - setValue(value) - } - - override fun toByte(): Byte = toLong().toByte() - - override fun toShort(): Short = toLong().toShort() -} - -internal fun cfIndex(value: Int): CFIndex = CFIndex(value.toLong()) - -internal class CFStringRef : PointerByReference() - -internal fun String.toCFString(): CFStringRef { - val chars = toCharArray() - return macFSEventsApi.CFStringCreateWithCharacters(null, chars, cfIndex(chars.size)) -} \ No newline at end of file diff --git a/plugins/agent-workbench/lib-agent/filewatch/src/impl/watchservice/jna/MacKQueueApi.kt b/plugins/agent-workbench/lib-agent/filewatch/src/impl/watchservice/jna/MacKQueueApi.kt deleted file mode 100644 index 2d43a86a31d6..000000000000 --- a/plugins/agent-workbench/lib-agent/filewatch/src/impl/watchservice/jna/MacKQueueApi.kt +++ /dev/null @@ -1,62 +0,0 @@ -// 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.filewatch.impl.watchservice.jna - -import com.sun.jna.LastErrorException -import com.sun.jna.Library -import com.sun.jna.Native -import com.sun.jna.NativeLong -import com.sun.jna.Pointer -import com.sun.jna.Structure - -internal interface MacKQueueApi : Library { - @Throws(LastErrorException::class) - fun kqueue(): Int - - @Throws(LastErrorException::class) - fun kevent( - kq: Int, - changelist: KEvent?, - nchanges: Int, - eventlist: KEvent?, - nevents: Int, - timeout: Pointer?, - ): Int - - @Throws(LastErrorException::class) - fun open(path: String, flags: Int): Int - - @Throws(LastErrorException::class) - fun close(fd: Int): Int -} - -@Structure.FieldOrder("ident", "filter", "flags", "fflags", "data", "udata") -@Suppress("unused") -internal class KEvent() : Structure() { - @JvmField var ident: NativeLong = NativeLong(0) - @JvmField var filter: Short = 0 - @JvmField var flags: Short = 0 - @JvmField var fflags: Int = 0 - @JvmField var data: NativeLong = NativeLong(0) - @JvmField var udata: Pointer? = null - - constructor(ident: Int, filter: Short, flags: Short, fflags: Int) : this() { - this.ident = NativeLong(ident.toLong()) - this.filter = filter - this.flags = flags - this.fflags = fflags - } -} - -internal val macKQueueApi: MacKQueueApi = Native.load("System", MacKQueueApi::class.java) - -internal const val K_QUEUE_OPEN_EVENT_ONLY: Int = 0x8000 - -internal const val K_EVENT_FILTER_VNODE: Short = -4 -internal const val K_EVENT_ADD: Short = 0x0001 -internal const val K_EVENT_CLEAR: Short = 0x0020 - -internal const val K_NOTE_DELETE: Int = 0x00000001 -internal const val K_NOTE_WRITE: Int = 0x00000002 -internal const val K_NOTE_EXTEND: Int = 0x00000004 -internal const val K_NOTE_RENAME: Int = 0x00000020 -internal const val K_NOTE_REVOKE: Int = 0x00000040 diff --git a/plugins/agent-workbench/lib-agent/filewatch/testSrc/AgentWorkbenchDirectoryWatcherTest.kt b/plugins/agent-workbench/lib-agent/filewatch/testSrc/AgentWorkbenchDirectoryWatcherTest.kt deleted file mode 100644 index c6758eb67286..000000000000 --- a/plugins/agent-workbench/lib-agent/filewatch/testSrc/AgentWorkbenchDirectoryWatcherTest.kt +++ /dev/null @@ -1,267 +0,0 @@ -// 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.filewatch - -import com.intellij.platform.ai.agent.filewatch.impl.DirectoryChangeEvent -import com.intellij.platform.ai.agent.filewatch.impl.DirectoryChangeListener -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeout -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.Timeout -import java.util.concurrent.TimeUnit -import org.junit.jupiter.api.io.TempDir -import java.nio.file.Path -import java.util.concurrent.LinkedBlockingQueue -import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.seconds - -@Timeout(value = 2, unit = TimeUnit.MINUTES) -class AgentWorkbenchDirectoryWatcherTest { - @TempDir - lateinit var tempDir: Path - - @Test - fun pathlessOverflowIsDelivered() = runBlocking(Dispatchers.Default) { - val watchLoop = FakeWatchLoop() - val events = LinkedBlockingQueue() - val failures = LinkedBlockingQueue() - - AgentWorkbenchDirectoryWatcher( - roots = listOf(tempDir), - scope = this, - onWatchEvent = events::add, - onFailure = failures::add, - watchLoopFactory = { _, listener -> - watchLoop.listener = listener - watchLoop - }, - ).use { watcher -> - watchLoop.offer(DirectoryChangeEvent(DirectoryChangeEvent.EventType.OVERFLOW, false, null, 1, tempDir.toAbsolutePath())) - - val event = withTimeout(5.seconds) { - events.take() - } - - assertThat(event.eventType).isEqualTo(AgentWorkbenchWatchEventType.OVERFLOW) - assertThat(event.path).isNull() - assertThat(event.rootPath).isEqualTo(tempDir.toAbsolutePath()) - assertThat(event.count).isEqualTo(1) - assertThat(failures).isEmpty() - watcher.close() - } - } - - @Test - fun rootPathIsPreservedForDeliveredEvents() { - runBlocking(Dispatchers.Default) { - val watchLoop = FakeWatchLoop() - val events = LinkedBlockingQueue() - val root = tempDir.toAbsolutePath() - val changedPath = root.resolve("session.jsonl") - - AgentWorkbenchDirectoryWatcher( - roots = listOf(tempDir), - scope = this, - onWatchEvent = events::add, - watchLoopFactory = { _, listener -> - watchLoop.listener = listener - watchLoop - }, - ).use { - watchLoop.offer(DirectoryChangeEvent(DirectoryChangeEvent.EventType.CREATE, false, changedPath, 1, root)) - - val event = withTimeout(5.seconds) { - events.take() - } - - assertThat(event.eventType).isEqualTo(AgentWorkbenchWatchEventType.CREATE) - assertThat(event.path).isEqualTo(changedPath) - assertThat(event.rootPath).isEqualTo(root) - } - } - } - - @Test - fun closeSuppressesLateEventsAndFailures() = runBlocking(Dispatchers.Default) { - val watchLoop = FakeWatchLoop() - val events = LinkedBlockingQueue() - val failures = LinkedBlockingQueue() - val root = tempDir.toAbsolutePath() - - AgentWorkbenchDirectoryWatcher( - roots = listOf(tempDir), - scope = this, - onWatchEvent = events::add, - onFailure = failures::add, - watchLoopFactory = { _, listener -> - watchLoop.listener = listener - watchLoop - }, - ).use { watcher -> - watcher.close() - - watchLoop.offer(DirectoryChangeEvent(DirectoryChangeEvent.EventType.CREATE, false, root.resolve("late.jsonl"), 1, root)) - watchLoop.fail(IllegalStateException("late failure")) - delay(200.milliseconds) - - assertThat(events).isEmpty() - assertThat(failures).isEmpty() - } - } - - @Test - fun watcherWithNoValidRootsIsInactive() { - runBlocking(Dispatchers.Default) { - val watcher = AgentWorkbenchDirectoryWatcher( - roots = listOf(tempDir.resolve("missing")), - scope = this, - onWatchEvent = {}, - ) - - assertThat(watcher.isActive).isFalse() - } - } - - @Test - fun closeMakesWatcherInactive() { - runBlocking(Dispatchers.Default) { - val watchLoop = FakeWatchLoop() - val watcher = AgentWorkbenchDirectoryWatcher( - roots = listOf(tempDir), - scope = this, - onWatchEvent = {}, - watchLoopFactory = { _, listener -> - watchLoop.listener = listener - watchLoop - }, - ) - - assertThat(watcher.isActive).isTrue() - - watcher.closeAndJoin() - - assertThat(watcher.isActive).isFalse() - } - } - - @Test - fun completedWatchLoopMakesWatcherInactive() { - runBlocking(Dispatchers.Default) { - val watchLoop = CompletingWatchLoop() - val watcher = AgentWorkbenchDirectoryWatcher( - roots = listOf(tempDir), - scope = this, - onWatchEvent = {}, - watchLoopFactory = { _, _ -> watchLoop }, - ) - - withTimeout(5.seconds) { - watchLoop.completed.await() - } - - withTimeout(5.seconds) { - while (watcher.isActive) { - delay(10.milliseconds) - } - } - - assertThat(watcher.isActive).isFalse() - } - } - - @Test - fun failedWatchLoopRestartsWithSameRoots() { - runBlocking(Dispatchers.Default) { - val events = LinkedBlockingQueue() - val failures = LinkedBlockingQueue() - val restartedLoop = CompletableDeferred() - val root = tempDir.toAbsolutePath() - var createCount = 0 - - AgentWorkbenchDirectoryWatcher( - roots = listOf(tempDir), - scope = this, - onWatchEvent = events::add, - onFailure = failures::add, - watchLoopFactory = { _, listener -> - createCount += 1 - if (createCount == 1) { - FailingWatchLoop(IllegalStateException("watch loop failed")) - } - else { - FakeWatchLoop().also { loop -> - loop.listener = listener - restartedLoop.complete(loop) - } - } - }, - ).use { watcher -> - val failure = withTimeout(5.seconds) { failures.take() } - val loop = withTimeout(5.seconds) { restartedLoop.await() } - - loop.offer(DirectoryChangeEvent(DirectoryChangeEvent.EventType.CREATE, false, root.resolve("session.jsonl"), 1, root)) - val event = withTimeout(5.seconds) { events.take() } - - assertThat(failure).hasMessage("watch loop failed") - assertThat(event.path).isEqualTo(root.resolve("session.jsonl")) - assertThat(watcher.isActive).isTrue() - } - } - } -} - -private class CompletingWatchLoop : AgentWorkbenchWatchLoop { - val completed = CompletableDeferred() - - override suspend fun watch() { - completed.complete(Unit) - } - - override fun close() { - } -} - -private class FailingWatchLoop(private val error: Throwable) : AgentWorkbenchWatchLoop { - override suspend fun watch() { - throw error - } - - override fun close() { - } -} - -private class FakeWatchLoop : AgentWorkbenchWatchLoop { - private val events = LinkedBlockingQueue() - lateinit var listener: DirectoryChangeListener - - @Volatile - private var closed = false - - private object Stop - - fun offer(event: DirectoryChangeEvent) { - events.add(event) - } - - fun fail(e: Exception) { - events.add(e) - } - - override suspend fun watch() { - while (!closed) { - when (val event = events.take()) { - Stop -> return - is DirectoryChangeEvent -> listener.onEvent(event) - is Exception -> listener.onException(event) - } - } - } - - override fun close() { - closed = true - events.offer(Stop) - } -} diff --git a/plugins/agent-workbench/lib-agent/filewatch/testSrc/impl/DirectoryWatcherImplTest.kt b/plugins/agent-workbench/lib-agent/filewatch/testSrc/impl/DirectoryWatcherImplTest.kt deleted file mode 100644 index a5261040e4f4..000000000000 --- a/plugins/agent-workbench/lib-agent/filewatch/testSrc/impl/DirectoryWatcherImplTest.kt +++ /dev/null @@ -1,424 +0,0 @@ -// 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.filewatch.impl - -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.AbstractWatchKey -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.AbstractWatchService -import com.intellij.platform.ai.agent.filewatch.impl.watchservice.WatchablePath -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeout -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.assertThatThrownBy -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.Timeout -import org.junit.jupiter.api.io.TempDir -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.StandardWatchEventKinds.ENTRY_CREATE -import java.nio.file.StandardWatchEventKinds.ENTRY_DELETE -import java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY -import java.nio.file.StandardWatchEventKinds.OVERFLOW -import java.nio.file.WatchEvent -import java.nio.file.WatchEvent.Modifier -import java.nio.file.WatchKey -import java.nio.file.WatchService -import java.nio.file.Watchable -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicInteger -import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.seconds - -@Timeout(value = 2, unit = TimeUnit.MINUTES) -class DirectoryWatcherImplTest { - @TempDir - lateinit var tempDir: Path - - @Test - fun watchThrowsWhenWatcherIsAlreadyClosed() { - runBlocking(Dispatchers.IO) { - val watcher = DirectoryWatcher(listOf(tempDir), EventRecorder()) - - watcher.close() - - assertThatThrownBy { runBlocking { watcher.watch() } } - .isInstanceOf(IllegalStateException::class.java) - } - } - - @Test - fun watchReturnsWhenWatcherIsClosed() = runBlocking(Dispatchers.IO) { - val watcher = DirectoryWatcher(listOf(tempDir), EventRecorder()) - - val completed = CompletableDeferred() - val watchJob = launch { - watcher.watch() - completed.complete(Unit) - } - try { - delay(100.milliseconds) - watcher.close() - - withTimeout(5.seconds) { - completed.await() - } - } - finally { - watchJob.cancelAndJoin() - } - } - - @Test - fun deletingNestedFilesEmitsDeleteEvents() { - runBlocking(Dispatchers.IO) { - val parentDir = Files.createDirectory(tempDir.resolve("parent")) - val childDir = Files.createDirectory(parentDir.resolve("child")) - val firstFile = Files.createFile(parentDir.resolve("first.jsonl")) - val secondFile = Files.createFile(childDir.resolve("second.jsonl")) - val recorder = EventRecorder() - val watcher = DirectoryWatcher(listOf(tempDir), recorder) - val watchJob = launch { watcher.watch() } - try { - delay(100.milliseconds) - Files.delete(secondFile) - Files.delete(firstFile) - - recorder.awaitEvent { event -> event.eventType == DirectoryChangeEvent.EventType.DELETE && event.path == secondFile } - recorder.awaitEvent { event -> event.eventType == DirectoryChangeEvent.EventType.DELETE && event.path == firstFile } - } - finally { - watcher.close() - watchJob.cancelAndJoin() - } - } - } - - @Test - fun repeatedStartAndCloseDoesNotCrash() { - repeat(3) { - val watcher = DirectoryWatcher(listOf(tempDir), EventRecorder()) - watcher.close() - } - } - - @Test - fun invalidatingOnlyWatchKeyFailsWatch() { - runBlocking(Dispatchers.IO) { - val watchService = TestWatchService(queueSize = 8) - val watcher = DirectoryWatcher( - listOf(tempDir), - EventRecorder(), - watchService, - watchablePathFactory = { path -> WatchablePath(path) }, - ) - val failure = CompletableDeferred() - val watchJob = launch { - try { - watcher.watch() - } - catch (t: Throwable) { - failure.complete(t) - } - } - try { - val key = withTimeout(5.seconds) { - watchService.awaitRegisteredKey() - } - - key.cancel() - key.signalOverflow(tempDir) - - val error = withTimeout(5.seconds) { - failure.await() - } - - assertThat(error) - .isInstanceOf(IllegalStateException::class.java) - .hasMessage("No more directories left to watch") - } - finally { - watcher.close() - watchJob.cancelAndJoin() - } - } - } - - @Test - fun unsupportedFileTreeModifierFallbackWalksTreeOnce() { - runBlocking(Dispatchers.IO) { - val childDir = tempDir.resolve("child") - val nestedDir = childDir.resolve("nested") - val fileTreeVisitor = FakeFileTreeVisitor(listOf(tempDir, childDir, nestedDir)) - val watchService = FileTreeUnsupportedWatchService() - val watcher = DirectoryWatcher( - listOf(tempDir), - EventRecorder(), - watchService, - fileTreeVisitor, - watchablePathFactory = { path -> FakeWatchablePath(path, watchService) }, - ) - - val completed = CompletableDeferred() - val watchJob = launch { - watcher.watch() - completed.complete(Unit) - } - try { - withTimeout(5.seconds) { - watchService.awaitRegisteredDirectories(3) - } - - assertThat(fileTreeVisitor.visitCount.get()).isEqualTo(1) - assertThat(watchService.registeredDirectories).containsExactly(tempDir, childDir, nestedDir) - } - finally { - watcher.close() - watchJob.cancelAndJoin() - } - } - } - - @Test - fun creatingDirectoryDoesNotReportNewFilesAsModified() { - runBlocking(Dispatchers.IO) { - val createdDirectory = tempDir.resolve("created") - val createdFile = createdDirectory.resolve("session.jsonl") - val fileTreeVisitor = MutableFakeFileTreeVisitor(listOf(tempDir), emptyList()) - val watchService = FileTreeUnsupportedWatchService() - val recorder = EventRecorder() - val watcher = DirectoryWatcher( - listOf(tempDir), - recorder, - watchService, - fileTreeVisitor, - watchablePathFactory = { path -> FakeWatchablePath(path, watchService) }, - ) - - val watchJob = launch { watcher.watch() } - try { - withTimeout(5.seconds) { - watchService.awaitRegisteredDirectories(1) - } - Files.createDirectory(createdDirectory) - Files.writeString(createdFile, "created") - fileTreeVisitor.directories = listOf(createdDirectory) - fileTreeVisitor.files = listOf(createdFile) - watchService.signal(tempDir, FakeWatchEvent(ENTRY_CREATE, 1, createdDirectory.fileName)) - - recorder.awaitEvent { event -> event.eventType == DirectoryChangeEvent.EventType.CREATE && event.path == createdDirectory } - recorder.awaitEvent { event -> event.eventType == DirectoryChangeEvent.EventType.CREATE && event.path == createdFile } - - assertThat(recorder.drainEvents().any { event -> - event.eventType == DirectoryChangeEvent.EventType.MODIFY && event.path == createdFile - }).isFalse() - } - finally { - watcher.close() - watchJob.cancelAndJoin() - } - } - } - - @Test - fun overflowOnlyEventsResignalWatchKeyOnReset() { - val watchService = TestWatchService(queueSize = 1) - val key = watchService.register(WatchablePath(tempDir), listOf(ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY)) - - key.signalEvent(ENTRY_CREATE, tempDir.resolve("file")) - - val firstEvents = key.pollEvents() - - assertThat(firstEvents.map { it.kind() }).containsExactly(ENTRY_CREATE) - key.signalOverflow(tempDir) - assertThat(key.reset()).isTrue() - - val secondEvents = watchService.poll()!!.pollEvents() - - assertThat(secondEvents.map { it.kind() }).containsExactly(OVERFLOW) - } - -} - -private class TestWatchService(private val queueSize: Int) : AbstractWatchService() { - @Volatile - private var registeredKey: AbstractWatchKey? = null - - override fun register(watchable: WatchablePath, eventTypes: Iterable>): AbstractWatchKey { - return AbstractWatchKey(this, watchable, eventTypes, queueSize).also { key -> - registeredKey = key - } - } - - fun awaitRegisteredKey(): AbstractWatchKey { - while (true) { - registeredKey?.let { key -> return key } - Thread.sleep(10) - } - } - -} - -private class FakeFileTreeVisitor(private val directories: List) : FileTreeVisitor { - val visitCount = AtomicInteger() - - override fun recursiveVisitFiles(root: Path, directoryConsumer: (Path) -> Unit, fileConsumer: (Path) -> Unit) { - visitCount.incrementAndGet() - for (directory in directories) { - directoryConsumer(directory) - } - } -} - -private class MutableFakeFileTreeVisitor( - @Volatile var directories: List, - @Volatile var files: List, -) : FileTreeVisitor { - override fun recursiveVisitFiles(root: Path, directoryConsumer: (Path) -> Unit, fileConsumer: (Path) -> Unit) { - for (directory in directories) { - directoryConsumer(directory) - } - for (file in files) { - fileConsumer(file) - } - } -} - -private class FakeWatchablePath(private val path: Path, private val watchService: FileTreeUnsupportedWatchService) : Watchable { - override fun register(watcher: WatchService, events: Array>, vararg modifiers: Modifier): WatchKey { - check(watcher === watchService) - if (modifiers.isNotEmpty()) { - throw UnsupportedOperationException() - } - return watchService.register(path, events) - } - - override fun register(watcher: WatchService, vararg events: WatchEvent.Kind<*>): WatchKey { - return register(watcher, events) - } -} - -private class FileTreeUnsupportedWatchService : WatchService { - private val keys = LinkedBlockingQueue() - private val registered = ArrayList() - - val registeredDirectories: List - get() = ArrayList(registered) - - fun awaitRegisteredDirectories(count: Int) { - while (registered.size < count) { - Thread.sleep(10) - } - } - - fun register(directory: Path, @Suppress("unused") events: Array>): FileTreeUnsupportedWatchKey { - registered.add(directory) - return FileTreeUnsupportedWatchKey(directory, keys).also { key -> - watchKeys[directory] = key - } - } - - private val watchKeys = HashMap() - - fun signal(directory: Path, event: WatchEvent<*>) { - watchKeys.getValue(directory).post(event) - } - - override fun poll(): WatchKey? = keys.poll() - - override fun poll(timeout: Long, unit: TimeUnit): WatchKey? = keys.poll(timeout, unit) - - override fun take(): WatchKey = keys.take() - - override fun close() { - } -} - -private class FileTreeUnsupportedWatchKey( - private val directory: Path, - private val keys: LinkedBlockingQueue, -) : WatchKey { - private val events = ArrayList>() - private var pendingOverflowCount = 0 - private var signalled = false - - fun post(event: WatchEvent<*>) { - events.add(event) - signal() - } - - private fun signal() { - if (!signalled) { - signalled = true - keys.add(this) - } - } - - override fun isValid(): Boolean = true - - override fun pollEvents(): List> { - val result = ArrayList(events) - events.clear() - if (pendingOverflowCount != 0) { - result.add(FakeWatchEvent(OVERFLOW, pendingOverflowCount, null)) - pendingOverflowCount = 0 - } - return result - } - - override fun reset(): Boolean { - signalled = false - if (events.isNotEmpty() || pendingOverflowCount != 0) { - signal() - } - return true - } - - override fun cancel() { - } - - override fun watchable(): Watchable = directory -} - -private data class FakeWatchEvent( - private val kind: WatchEvent.Kind, - private val count: Int, - private val context: T?, -) : WatchEvent { - override fun kind(): WatchEvent.Kind = kind - - override fun count(): Int = count - - override fun context(): T? = context -} - -private class EventRecorder : DirectoryChangeListener { - private val events = LinkedBlockingQueue() - - override suspend fun onEvent(event: DirectoryChangeEvent) { - events.add(event) - } - - suspend fun awaitEvent(predicate: (DirectoryChangeEvent) -> Boolean): DirectoryChangeEvent { - return withTimeout(5.seconds) { - lateinit var matchingEvent: DirectoryChangeEvent - while (true) { - val event = events.take() - if (predicate(event)) { - matchingEvent = event - break - } - } - matchingEvent - } - } - - fun drainEvents(): List { - val result = ArrayList() - events.drainTo(result) - return result - } -} diff --git a/plugins/agent-workbench/lib-agent/filewatch/testSrc/impl/watchservice/MacOSXListeningWatchServiceTest.kt b/plugins/agent-workbench/lib-agent/filewatch/testSrc/impl/watchservice/MacOSXListeningWatchServiceTest.kt deleted file mode 100644 index bf0ee5da4ed6..000000000000 --- a/plugins/agent-workbench/lib-agent/filewatch/testSrc/impl/watchservice/MacOSXListeningWatchServiceTest.kt +++ /dev/null @@ -1,169 +0,0 @@ -// 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.filewatch.impl.watchservice - -import com.intellij.platform.ai.agent.filewatch.agentWorkbenchImmediateFileChangeFlow -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeout -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Assumptions.assumeTrue -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.Timeout -import org.junit.jupiter.api.io.TempDir -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.StandardOpenOption -import java.nio.file.StandardWatchEventKinds.ENTRY_CREATE -import java.nio.file.StandardWatchEventKinds.ENTRY_DELETE -import java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY -import java.nio.file.WatchEvent -import java.nio.file.WatchKey -import java.util.concurrent.TimeUnit -import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.seconds - -@Timeout(value = 2, unit = TimeUnit.MINUTES) -class MacOSXListeningWatchServiceTest { - @TempDir - lateinit var tempDir: Path - private val pendingEvents = ArrayList>() - - @Test - fun createModifyDeleteEventsAreDeliveredByNativeMacWatcher() = runBlocking(Dispatchers.IO) { - assumeMacOS() - val watchService = createWatchService() - val watchKey = WatchablePath(tempDir).register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE) - - watchService.use { - delay(100.milliseconds) - val file = tempDir.resolve("session.jsonl") - - Files.writeString(file, "created") - awaitEvent(watchService, watchKey, ENTRY_CREATE, file) - - Files.writeString(file, "modified") - awaitEvent(watchService, watchKey, ENTRY_MODIFY, file) - - Files.delete(file) - awaitEvent(watchService, watchKey, ENTRY_DELETE, file) - } - } - - @Test - fun nestedFileDeleteIsDeliveredByNativeMacWatcher() = runBlocking(Dispatchers.IO) { - assumeMacOS() - val watchService = createWatchService() - val watchKey = WatchablePath(tempDir).register(watchService, ENTRY_CREATE, ENTRY_DELETE) - - watchService.use { - delay(100.milliseconds) - val directory = Files.createDirectory(tempDir.resolve("session")) - val file = directory.resolve("nested.jsonl") - - Files.writeString(file, "created") - awaitEvent(watchService, watchKey, ENTRY_CREATE, file) - - Files.delete(file) - awaitEvent(watchService, watchKey, ENTRY_DELETE, file) - } - } - - @Test - fun immediateFileChangeFlowDeliversLongLivedAppendFdBeforeClose(): Unit = runBlocking(Dispatchers.IO) { - // Codex keeps the rollout JSONL fd open for the entire session lifetime. FSEvents alone - // withholds MODIFY events until the fd is closed, so active threadView tabs subscribe to their - // concrete session file with kqueue vnode events. - assumeMacOS() - val file = tempDir.resolve("rollout-long-lived.jsonl") - Files.createFile(file) - val expectedPath = file.toAbsolutePath().normalize() - val changes = Channel(Channel.UNLIMITED) - val watchJob = launch { - agentWorkbenchImmediateFileChangeFlow(listOf(file)).collect { path -> - changes.trySend(path) - } - } - - try { - delay(200.milliseconds) - Files.newOutputStream(file, StandardOpenOption.APPEND).use { stream -> - stream.write("line1\n".toByteArray()) - stream.flush() - assertThat(withTimeout(5.seconds) { changes.receive() }).isEqualTo(expectedPath) - - stream.write("line2\n".toByteArray()) - stream.flush() - assertThat(withTimeout(5.seconds) { changes.receive() }).isEqualTo(expectedPath) - } - } - finally { - watchJob.cancelAndJoin() - changes.close() - } - } - - @Test - fun repeatedNativeStartAndCloseDoesNotCrash() { - assumeMacOS() - repeat(3) { - createWatchService().use { watchService -> - WatchablePath(tempDir).register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE) - } - } - } - - private fun createWatchService(): MacOSXListeningWatchService { - return MacOSXListeningWatchService(object : MacOSXListeningWatchService.Config { - override val latency: Double = 0.1 - }) - } - - private fun awaitEvent( - watchService: MacOSXListeningWatchService, - expectedKey: WatchKey, - expectedKind: WatchEvent.Kind, - expectedPath: Path, - ) { - val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5) - while (System.nanoTime() < deadline) { - val pendingEvent = pendingEvents.findAndRemove(expectedKind, expectedPath) - if (pendingEvent != null) { - assertThat(pendingEvent.context()).isEqualTo(expectedPath) - return - } - val key = watchService.poll(100, TimeUnit.MILLISECONDS) ?: continue - assertThat(key).isSameAs(expectedKey) - val events = key.pollEvents() - val event = events.firstOrNull { event -> event.kind() === expectedKind && event.context() == expectedPath } - if (event != null) { - assertThat(event.context()).isEqualTo(expectedPath) - pendingEvents.addAll(events - event) - key.reset() - return - } - pendingEvents.addAll(events) - key.reset() - } - throw AssertionError("Expected $expectedKind event for $expectedPath") - } - - private fun ArrayList>.findAndRemove(kind: WatchEvent.Kind, path: Path): WatchEvent<*>? { - val iterator = iterator() - while (iterator.hasNext()) { - val event = iterator.next() - if (event.kind() === kind && event.context() == path) { - iterator.remove() - return event - } - } - return null - } - - private fun assumeMacOS() { - assumeTrue(System.getProperty("os.name").contains("mac", ignoreCase = true)) - } -} diff --git a/plugins/agent-workbench/lib-agent/json/BUILD.bazel b/plugins/agent-workbench/lib-agent/json/BUILD.bazel deleted file mode 100644 index c0bd956ceb93..000000000000 --- a/plugins/agent-workbench/lib-agent/json/BUILD.bazel +++ /dev/null @@ -1,132 +0,0 @@ -load("@community//build:tests-options.bzl", "jps_test") -load("@rules_jvm//:jvm.bzl", "jvm_library") - -### auto-generated section `build intellij.platform.ai.agent.json` start - -jvm_library( - name = "json", - srcs = glob( - [ - "src/**/*.kt", - "src/**/*.java", - "src/**/*.form", - ], - allow_empty = True, - ), - module_name = "intellij.platform.ai.agent.json", - resource_strip_prefix = "resources", - resources = glob( - ["resources/**/*"], - ), - visibility = ["//visibility:public"], - deps = [ - # do not sort, - "@lib//:kotlin-stdlib", - "//libraries/kotlinx/coroutines/core", - "//platform/core-api:core", - "//platform/util", - "//plugins/agent-workbench/lib-agent/filewatch", - "//libraries/jackson3/jackson:libraries-jackson3", - ], -) - -jvm_library( - name = "json_test_lib", - testonly = True, - srcs = glob( - [], - allow_empty = True, - ), - module_name = "intellij.platform.ai.agent.json", - visibility = ["//visibility:public"], - runtime_deps = [ - # do not sort, - ":json", - "//libraries/kotlinx/coroutines/core:core_test_lib", - "//platform/core-api:core_test_lib", - "//platform/util:util_test_lib", - "//plugins/agent-workbench/lib-agent/filewatch:filewatch_test_lib", - "//libraries/jackson3/jackson:libraries-jackson3_test_lib", - "//libraries/junit5", - "//libraries/junit5:junit5_test_lib", - "//libraries/assertj-core", - "//libraries/assertj-core:assertj-core_test_lib", - ], -) -### auto-generated section `build intellij.platform.ai.agent.json` end - -### auto-generated section `iml intellij.platform.ai.agent.json` start -exports_files( - [ - "intellij.platform.ai.agent.json.iml", - ], - visibility = ["//visibility:public"], -) -### auto-generated section `iml intellij.platform.ai.agent.json` end - -### auto-generated section `build intellij.platform.ai.agent.json.tests` start - -jvm_library( - name = "ai-agent-json-tests", - srcs = glob( - [], - allow_empty = True, - ), - module_name = "intellij.platform.ai.agent.json.tests", - visibility = ["//visibility:public"], -) - -jvm_library( - name = "ai-agent-json-tests_test_lib", - testonly = True, - srcs = glob( - [ - "testSrc/**/*.kt", - "testSrc/**/*.java", - "testSrc/**/*.form", - ], - allow_empty = True, - ), - associates = [ - "//plugins/agent-workbench/lib-agent/json", - "//plugins/agent-workbench/lib-agent/json:json_test_lib", - ], - visibility = ["//visibility:public"], - runtime_deps = [":ai-agent-json-tests"], - deps = [ - # do not sort, - "@lib//:kotlin-stdlib", - "//libraries/kotlinx/coroutines/core", - "//libraries/kotlinx/coroutines/core:core_test_lib", - "//platform/core-api:core", - "//platform/core-api:core_test_lib", - "//platform/util", - "//platform/util:util_test_lib", - "//plugins/agent-workbench/lib-agent/filewatch", - "//plugins/agent-workbench/lib-agent/filewatch:filewatch_test_lib", - "//libraries/jackson3/jackson:libraries-jackson3", - "//libraries/jackson3/jackson:libraries-jackson3_test_lib", - "//libraries/junit5", - "//libraries/junit5:junit5_test_lib", - "//libraries/assertj-core", - "//libraries/assertj-core:assertj-core_test_lib", - ], -) -### auto-generated section `build intellij.platform.ai.agent.json.tests` end - -### auto-generated section `iml intellij.platform.ai.agent.json.tests` start -exports_files( - [ - "intellij.platform.ai.agent.json.tests.iml", - ], - visibility = ["//visibility:public"], -) -### auto-generated section `iml intellij.platform.ai.agent.json.tests` end - -### auto-generated section `test intellij.platform.ai.agent.json.tests` start - -jps_test( - name = "ai-agent-json-tests_test", - runtime_deps = [":ai-agent-json-tests_test_lib"], -) -### auto-generated section `test intellij.platform.ai.agent.json.tests` end diff --git a/plugins/agent-workbench/lib-agent/json/api-dump.txt b/plugins/agent-workbench/lib-agent/json/api-dump.txt deleted file mode 100644 index 23022822c08a..000000000000 --- a/plugins/agent-workbench/lib-agent/json/api-dump.txt +++ /dev/null @@ -1,97 +0,0 @@ -f:com.intellij.platform.ai.agent.json.WorkbenchJsonParsingUtilsKt -- sf:createJsonGenerator(tools.jackson.core.json.JsonFactory,java.io.OutputStream):tools.jackson.core.JsonGenerator -- sf:createJsonGenerator(tools.jackson.core.json.JsonFactory,java.io.Writer):tools.jackson.core.JsonGenerator -- sf:createJsonGenerator(tools.jackson.core.json.JsonFactory,java.io.Writer,tools.jackson.core.PrettyPrinter):tools.jackson.core.JsonGenerator -- sf:createJsonParser(tools.jackson.core.json.JsonFactory,java.io.InputStream):tools.jackson.core.JsonParser -- sf:createJsonParser(tools.jackson.core.json.JsonFactory,java.io.Reader):tools.jackson.core.JsonParser -- sf:createJsonParser(tools.jackson.core.json.JsonFactory,java.lang.String):tools.jackson.core.JsonParser -- sf:createJsonParser(tools.jackson.core.json.JsonFactory,B[]):tools.jackson.core.JsonParser -- sf:forEachJsonObjectField(tools.jackson.core.JsonParser,kotlin.jvm.functions.Function1):V -- sf:readJsonLongOrNull(tools.jackson.core.JsonParser):java.lang.Long -- sf:readJsonStringOrNull(tools.jackson.core.JsonParser):java.lang.String -f:com.intellij.platform.ai.agent.json.WorkbenchJsonlScanner -- sf:INSTANCE:com.intellij.platform.ai.agent.json.WorkbenchJsonlScanner -- f:removeLines(java.nio.file.Path,tools.jackson.core.json.JsonFactory,kotlin.jvm.functions.Function1):V -- f:scanJsonObjects(java.nio.file.Path,tools.jackson.core.json.JsonFactory,I,kotlin.jvm.functions.Function0,kotlin.jvm.functions.Function2):java.lang.Object -- bs:scanJsonObjects$default(com.intellij.platform.ai.agent.json.WorkbenchJsonlScanner,java.nio.file.Path,tools.jackson.core.json.JsonFactory,I,kotlin.jvm.functions.Function0,kotlin.jvm.functions.Function2,I,java.lang.Object):java.lang.Object -- f:scanTailLines(java.nio.file.Path,tools.jackson.core.json.JsonFactory,J,kotlin.jvm.functions.Function0,kotlin.jvm.functions.Function2):java.lang.Object -- bs:scanTailLines$default(com.intellij.platform.ai.agent.json.WorkbenchJsonlScanner,java.nio.file.Path,tools.jackson.core.json.JsonFactory,J,kotlin.jvm.functions.Function0,kotlin.jvm.functions.Function2,I,java.lang.Object):java.lang.Object -f:com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionCachedFile -- f:lastModifiedNs:J -- f:parsedValue:java.lang.Object -- f:sizeBytes:J -- (J,J,java.lang.Object):V -- f:component1():J -- f:component2():J -- f:component3():java.lang.Object -- f:copy(J,J,java.lang.Object):com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionCachedFile -- bs:copy$default(com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionCachedFile,J,J,java.lang.Object,I,java.lang.Object):com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionCachedFile -- equals(java.lang.Object):Z -- hashCode():I -f:com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionChangeSet -- f:changedPaths:java.util.Set -- f:requiresFullRescan:Z -- ():V -- (java.util.Set,Z):V -- b:(java.util.Set,Z,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- f:component1():java.util.Set -- f:component2():Z -- f:copy(java.util.Set,Z):com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionChangeSet -- bs:copy$default(com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionChangeSet,java.util.Set,Z,I,java.lang.Object):com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionChangeSet -- equals(java.lang.Object):Z -- hashCode():I -f:com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionChangeSetKt -- sf:normalizeFileBackedSessionPath(java.nio.file.Path):java.nio.file.Path -- sf:toFileBackedSessionPathKey(java.nio.file.Path):java.lang.String -f:com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionEventClassifierKt -- sf:classifyFileBackedSessionEvent(com.intellij.platform.ai.agent.filewatch.AgentWorkbenchWatchEvent,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function1):com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionChangeSet -f:com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionFileStat -- f:lastModifiedNs:J -- f:path:java.nio.file.Path -- f:pathKey:java.lang.String -- f:sizeBytes:J -- (java.lang.String,java.nio.file.Path,J,J):V -- f:component1():java.lang.String -- f:component2():java.nio.file.Path -- f:component3():J -- f:component4():J -- f:copy(java.lang.String,java.nio.file.Path,J,J):com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionFileStat -- bs:copy$default(com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionFileStat,java.lang.String,java.nio.file.Path,J,J,I,java.lang.Object):com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionFileStat -- equals(java.lang.Object):Z -- hashCode():I -f:com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionInvalidationState -- (kotlin.jvm.functions.Function1):V -- f:applyParsedUpdates(java.util.Map):V -- f:clear():V -- f:markDirty(com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionChangeSet):I -- f:planRescan(java.util.Map):com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionRescanPlan -- f:snapshotCachedFiles():java.util.Map -f:com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionInvalidationStateKt -- sf:buildFileBackedSessionFileStat(java.nio.file.Path,J):com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionFileStat -- bs:buildFileBackedSessionFileStat$default(java.nio.file.Path,J,I,java.lang.Object):com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionFileStat -f:com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionRescanPlan -- f:dirtyPathCount:I -- f:filesToParse:java.util.List -- f:fullRescan:Z -- f:removedAny:Z -- (java.util.List,Z,Z,I):V -- f:component1():java.util.List -- f:component2():Z -- f:component3():Z -- f:component4():I -- f:copy(java.util.List,Z,Z,I):com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionRescanPlan -- bs:copy$default(com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionRescanPlan,java.util.List,Z,Z,I,I,java.lang.Object):com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionRescanPlan -- equals(java.lang.Object):Z -- hashCode():I -f:com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionUpdatesFlowKt -- sf:createFileBackedSessionChangeFlow(com.intellij.openapi.diagnostic.Logger,java.lang.String,kotlin.jvm.functions.Function0,Z,kotlin.jvm.functions.Function2):kotlinx.coroutines.flow.Flow -- bs:createFileBackedSessionChangeFlow$default(com.intellij.openapi.diagnostic.Logger,java.lang.String,kotlin.jvm.functions.Function0,Z,kotlin.jvm.functions.Function2,I,java.lang.Object):kotlinx.coroutines.flow.Flow -f:com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionWatcher -- java.lang.AutoCloseable -- (com.intellij.openapi.diagnostic.Logger,java.lang.String,com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionWatcherSpec,kotlinx.coroutines.CoroutineScope,kotlin.jvm.functions.Function1,java.lang.String):V -- b:(com.intellij.openapi.diagnostic.Logger,java.lang.String,com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionWatcherSpec,kotlinx.coroutines.CoroutineScope,kotlin.jvm.functions.Function1,java.lang.String,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- close():V -f:com.intellij.platform.ai.agent.json.filebacked.FileBackedSessionWatcherSpec -- (java.util.Collection,kotlin.jvm.functions.Function1):V -- f:getEventToChangeSet():kotlin.jvm.functions.Function1 -- f:getRoots():java.util.Collection diff --git a/plugins/agent-workbench/lib-agent/json/exposed-third-party-api.txt b/plugins/agent-workbench/lib-agent/json/exposed-third-party-api.txt deleted file mode 100644 index a19ab2693bd8..000000000000 --- a/plugins/agent-workbench/lib-agent/json/exposed-third-party-api.txt +++ /dev/null @@ -1,5 +0,0 @@ -kotlin/jvm/internal/DefaultConstructorMarker -tools/jackson/core/JsonGenerator -tools/jackson/core/JsonParser -tools/jackson/core/PrettyPrinter -tools/jackson/core/json/JsonFactory diff --git a/plugins/agent-workbench/lib-agent/json/intellij.platform.ai.agent.json.iml b/plugins/agent-workbench/lib-agent/json/intellij.platform.ai.agent.json.iml deleted file mode 100644 index 7994e2c0d979..000000000000 --- a/plugins/agent-workbench/lib-agent/json/intellij.platform.ai.agent.json.iml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/plugins/agent-workbench/lib-agent/json/intellij.platform.ai.agent.json.tests.iml b/plugins/agent-workbench/lib-agent/json/intellij.platform.ai.agent.json.tests.iml deleted file mode 100644 index 35c616dd907a..000000000000 --- a/plugins/agent-workbench/lib-agent/json/intellij.platform.ai.agent.json.tests.iml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/plugins/agent-workbench/lib-agent/json/resources/intellij.platform.ai.agent.json.xml b/plugins/agent-workbench/lib-agent/json/resources/intellij.platform.ai.agent.json.xml deleted file mode 100644 index e26d96bb6a7f..000000000000 --- a/plugins/agent-workbench/lib-agent/json/resources/intellij.platform.ai.agent.json.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/plugins/agent-workbench/lib-agent/json/src/WorkbenchJsonParsingUtils.kt b/plugins/agent-workbench/lib-agent/json/src/WorkbenchJsonParsingUtils.kt deleted file mode 100644 index bb47e10f5ee6..000000000000 --- a/plugins/agent-workbench/lib-agent/json/src/WorkbenchJsonParsingUtils.kt +++ /dev/null @@ -1,93 +0,0 @@ -// 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.json - -import tools.jackson.core.JsonGenerator -import tools.jackson.core.JsonParser -import tools.jackson.core.JsonToken -import tools.jackson.core.ObjectReadContext -import tools.jackson.core.ObjectWriteContext -import tools.jackson.core.PrettyPrinter -import tools.jackson.core.StreamWriteFeature -import tools.jackson.core.TokenStreamFactory -import tools.jackson.core.json.JsonFactory -import java.io.InputStream -import java.io.OutputStream -import java.io.Reader -import java.io.Writer - -fun JsonFactory.createJsonParser(content: String): JsonParser { - return createParser(ObjectReadContext.empty(), content) -} - -fun JsonFactory.createJsonParser(content: ByteArray): JsonParser { - return createParser(ObjectReadContext.empty(), content) -} - -fun JsonFactory.createJsonParser(input: InputStream): JsonParser { - return createParser(ObjectReadContext.empty(), input) -} - -fun JsonFactory.createJsonParser(reader: Reader): JsonParser { - return createParser(ObjectReadContext.empty(), reader) -} - -fun JsonFactory.createJsonGenerator(output: OutputStream): JsonGenerator { - return createGenerator(ObjectWriteContext.empty(), output).configure(StreamWriteFeature.AUTO_CLOSE_TARGET, false) -} - -fun JsonFactory.createJsonGenerator(writer: Writer): JsonGenerator { - return createGenerator(ObjectWriteContext.empty(), writer).configure(StreamWriteFeature.AUTO_CLOSE_TARGET, false) -} - -fun JsonFactory.createJsonGenerator(writer: Writer, prettyPrinter: PrettyPrinter): JsonGenerator { - return createGenerator(PrettyPrinterWriteContext(this, prettyPrinter), writer).configure(StreamWriteFeature.AUTO_CLOSE_TARGET, false) -} - -private class PrettyPrinterWriteContext( - private val factory: JsonFactory, - private val prettyPrinter: PrettyPrinter, -) : ObjectWriteContext.Base() { - override fun tokenStreamFactory(): TokenStreamFactory = factory - - override fun getPrettyPrinter(): PrettyPrinter = prettyPrinter -} - -inline fun forEachJsonObjectField(parser: JsonParser, onField: (String) -> Boolean) { - while (true) { - val token = parser.nextToken() ?: return - if (token == JsonToken.END_OBJECT) return - if (token != JsonToken.PROPERTY_NAME) { - parser.skipChildren() - continue - } - val fieldName = parser.currentName() - if (parser.nextToken() == null) return - if (!onField(fieldName)) return - } -} - -fun readJsonStringOrNull(parser: JsonParser): String? { - return when (parser.currentToken()) { - JsonToken.VALUE_STRING -> parser.string - JsonToken.VALUE_NUMBER_INT, JsonToken.VALUE_NUMBER_FLOAT -> parser.numberValue.toString() - JsonToken.VALUE_TRUE, JsonToken.VALUE_FALSE -> parser.booleanValue.toString() - JsonToken.VALUE_NULL -> null - else -> { - parser.skipChildren() - null - } - } -} - -fun readJsonLongOrNull(parser: JsonParser): Long? { - return when (parser.currentToken()) { - JsonToken.VALUE_NUMBER_INT -> parser.longValue - JsonToken.VALUE_NUMBER_FLOAT -> parser.doubleValue.toLong() - JsonToken.VALUE_STRING -> parser.string.toLongOrNull() ?: parser.string.toDoubleOrNull()?.toLong() - JsonToken.VALUE_NULL -> null - else -> { - parser.skipChildren() - null - } - } -} diff --git a/plugins/agent-workbench/lib-agent/json/src/WorkbenchJsonlScanner.kt b/plugins/agent-workbench/lib-agent/json/src/WorkbenchJsonlScanner.kt deleted file mode 100644 index 6835a5f2caa3..000000000000 --- a/plugins/agent-workbench/lib-agent/json/src/WorkbenchJsonlScanner.kt +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -@file:Suppress("PackageDirectoryMismatch", "unused") - -package com.intellij.platform.ai.agent.json - -import tools.jackson.core.JsonParser -import tools.jackson.core.JsonToken -import tools.jackson.core.json.JsonFactory -import java.io.BufferedReader -import java.io.InputStreamReader -import java.nio.channels.Channels -import java.nio.channels.FileChannel -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.StandardOpenOption - -object WorkbenchJsonlScanner { - fun scanJsonObjects( - path: Path, - jsonFactory: JsonFactory, - maxObjects: Int = Int.MAX_VALUE, - newState: () -> S, - onObject: (JsonParser, S) -> Boolean, - ): S { - if (maxObjects <= 0) return newState() - - val fastState = newState() - val parsedWithSingleParser = try { - parseWithSingleParser(path, jsonFactory, maxObjects, fastState, onObject) - true - } - catch (_: Throwable) { - false - } - if (parsedWithSingleParser) { - return fastState - } - - val fallbackState = newState() - parseLineByLine(path, jsonFactory, maxObjects, fallbackState, onObject) - return fallbackState - } - - fun scanTailLines( - path: Path, - jsonFactory: JsonFactory, - tailBytes: Long = 16_384L, - newState: () -> S, - onObject: (JsonParser, S) -> Boolean, - ): S { - val state = newState() - try { - FileChannel.open(path, StandardOpenOption.READ).use { channel -> - val fileSize = channel.size() - val seekPos = maxOf(0L, fileSize - tailBytes) - channel.position(seekPos) - val reader = BufferedReader(InputStreamReader(Channels.newInputStream(channel), Charsets.UTF_8)) - // If we seeked mid-file, skip the first partial line. - if (seekPos > 0) { - reader.readLine() - } - while (true) { - val line = reader.readLine() ?: break - val trimmed = line.trim() - if (trimmed.isEmpty()) continue - val shouldContinue = parseLineObject(trimmed, jsonFactory, state, onObject) ?: continue - if (!shouldContinue) break - } - } - } - catch (_: Exception) { - // Tail scan is best-effort; return whatever state we collected. - } - return state - } - - /** - * Removes lines from a JSONL file where [shouldRemove] returns `true`. - * Deletes the file entirely if no lines remain after removal. - * - * @param shouldRemove receives a [JsonParser] positioned at `START_OBJECT`; return `true` to remove the line - */ - fun removeLines( - path: Path, - jsonFactory: JsonFactory, - shouldRemove: (JsonParser) -> Boolean, - ) { - if (!Files.exists(path)) return - val lines = Files.readAllLines(path) - val remaining = lines.filter { line -> - val trimmed = line.trim() - if (trimmed.isEmpty()) return@filter false - val remove = try { - jsonFactory.createJsonParser(trimmed).use { parser -> - parser.nextToken() == JsonToken.START_OBJECT && shouldRemove(parser) - } - } - catch (_: Throwable) { - false - } - !remove - } - if (remaining.isEmpty()) { - Files.deleteIfExists(path) - } - else { - Files.writeString(path, remaining.joinToString("\n", postfix = "\n")) - } - } - - private fun parseWithSingleParser( - path: Path, - jsonFactory: JsonFactory, - maxObjects: Int, - state: S, - onObject: (JsonParser, S) -> Boolean, - ) { - Files.newBufferedReader(path).use { reader -> - jsonFactory.createJsonParser(reader).use { parser -> - var parsedObjects = 0 - while (parsedObjects < maxObjects) { - val token = parser.nextToken() ?: return - if (token != JsonToken.START_OBJECT) { - parser.skipChildren() - continue - } - - parsedObjects++ - if (!onObject(parser, state)) { - return - } - } - } - } - } - - private fun parseLineByLine( - path: Path, - jsonFactory: JsonFactory, - maxObjects: Int, - state: S, - onObject: (JsonParser, S) -> Boolean, - ) { - Files.newBufferedReader(path).use { reader -> - var parsedObjects = 0 - while (parsedObjects < maxObjects) { - val line = reader.readLine() ?: return - val trimmed = line.trim() - if (trimmed.isEmpty()) continue - - val shouldContinue = parseLineObject(trimmed, jsonFactory, state, onObject) - if (shouldContinue == null) { - continue - } - - parsedObjects++ - if (!shouldContinue) { - return - } - } - } - } - - private fun parseLineObject( - line: String, - jsonFactory: JsonFactory, - state: S, - onObject: (JsonParser, S) -> Boolean, - ): Boolean? { - return try { - jsonFactory.createJsonParser(line).use { parser -> - if (parser.nextToken() != JsonToken.START_OBJECT) { - return null - } - onObject(parser, state) - } - } - catch (_: Throwable) { - null - } - } -} diff --git a/plugins/agent-workbench/lib-agent/json/src/filebacked/FileBackedSessionChangeSet.kt b/plugins/agent-workbench/lib-agent/json/src/filebacked/FileBackedSessionChangeSet.kt deleted file mode 100644 index d463afc4bcc4..000000000000 --- a/plugins/agent-workbench/lib-agent/json/src/filebacked/FileBackedSessionChangeSet.kt +++ /dev/null @@ -1,28 +0,0 @@ -// 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.json.filebacked - -import java.nio.file.Path -import kotlin.io.path.invariantSeparatorsPathString - -data class FileBackedSessionChangeSet( - // Known files that must be reparsed regardless of file stat heuristics. - @JvmField val changedPaths: Set = emptySet(), - - // Overflow/ambiguous events where file-level attribution is not reliable. - @JvmField val requiresFullRescan: Boolean = false, - - // `changedPaths.isEmpty() && !requiresFullRescan` means "refresh ping": - // re-run stat-based scan without forcing full reparse. -) - -fun normalizeFileBackedSessionPath(path: Path): Path { - return runCatching { - path.toAbsolutePath().normalize() - }.getOrElse { - path.normalize() - } -} - -fun toFileBackedSessionPathKey(path: Path): String { - return normalizeFileBackedSessionPath(path).invariantSeparatorsPathString -} diff --git a/plugins/agent-workbench/lib-agent/json/src/filebacked/FileBackedSessionEventClassifier.kt b/plugins/agent-workbench/lib-agent/json/src/filebacked/FileBackedSessionEventClassifier.kt deleted file mode 100644 index 00e97ce9e6bb..000000000000 --- a/plugins/agent-workbench/lib-agent/json/src/filebacked/FileBackedSessionEventClassifier.kt +++ /dev/null @@ -1,44 +0,0 @@ -// 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.json.filebacked - -import com.intellij.platform.ai.agent.filewatch.AgentWorkbenchWatchEvent -import com.intellij.platform.ai.agent.filewatch.AgentWorkbenchWatchEventType -import java.nio.file.Path - -fun classifyFileBackedSessionEvent( - event: AgentWorkbenchWatchEvent, - isChangedPath: (Path) -> Boolean, - isRelevantPath: (Path) -> Boolean, - isRelevantRoot: (Path) -> Boolean, -): FileBackedSessionChangeSet? { - val eventPath = event.path - val rootPath = event.rootPath - - if (event.eventType == AgentWorkbenchWatchEventType.OVERFLOW) { - val isRelevantOverflow = when { - eventPath != null && isRelevantPath(eventPath) -> true - rootPath != null && isRelevantRoot(rootPath) -> true - eventPath == null && rootPath == null -> true - else -> false - } - return if (isRelevantOverflow) FileBackedSessionChangeSet(requiresFullRescan = true) else null - } - - if (eventPath != null && isChangedPath(eventPath)) { - return FileBackedSessionChangeSet(changedPaths = setOf(normalizeFileBackedSessionPath(eventPath))) - } - - val isRelevantEvent = when { - eventPath != null -> isRelevantPath(eventPath) - rootPath != null -> isRelevantRoot(rootPath) - else -> false - } - if (!isRelevantEvent) return null - - val isAmbiguousDirectoryEvent = event.isDirectory || eventPath == null - if (isAmbiguousDirectoryEvent) { - return FileBackedSessionChangeSet(requiresFullRescan = true) - } - - return FileBackedSessionChangeSet() -} diff --git a/plugins/agent-workbench/lib-agent/json/src/filebacked/FileBackedSessionInvalidationState.kt b/plugins/agent-workbench/lib-agent/json/src/filebacked/FileBackedSessionInvalidationState.kt deleted file mode 100644 index 687a99b9de41..000000000000 --- a/plugins/agent-workbench/lib-agent/json/src/filebacked/FileBackedSessionInvalidationState.kt +++ /dev/null @@ -1,147 +0,0 @@ -// 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.json.filebacked - -import java.nio.file.Files -import java.nio.file.Path -import java.util.concurrent.TimeUnit - -data class FileBackedSessionFileStat( - @JvmField val pathKey: String, - @JvmField val path: Path, - @JvmField val lastModifiedNs: Long, - @JvmField val sizeBytes: Long, -) - -/** - * Reads the stat (mtime, size) of [candidate] and returns a [FileBackedSessionFileStat], - * or `null` if the file is not a regular file, cannot be stat'd, or its mtime is below - * [minLastModifiedNs]. - */ -fun buildFileBackedSessionFileStat( - candidate: Path, - minLastModifiedNs: Long = Long.MIN_VALUE, -): FileBackedSessionFileStat? { - if (!Files.isRegularFile(candidate)) return null - val lastModifiedNs = try { - Files.getLastModifiedTime(candidate).to(TimeUnit.NANOSECONDS) - } - catch (_: Throwable) { - return null - } - if (lastModifiedNs < minLastModifiedNs) return null - val sizeBytes = try { - Files.size(candidate) - } - catch (_: Throwable) { - return null - } - val pathKey = toFileBackedSessionPathKey(candidate) - return FileBackedSessionFileStat( - pathKey = pathKey, - path = candidate, - lastModifiedNs = lastModifiedNs, - sizeBytes = sizeBytes, - ) -} - -data class FileBackedSessionCachedFile( - @JvmField val lastModifiedNs: Long, - @JvmField val sizeBytes: Long, - @JvmField val parsedValue: Parsed, -) - -data class FileBackedSessionRescanPlan( - @JvmField val filesToParse: List, - @JvmField val removedAny: Boolean, - @JvmField val fullRescan: Boolean, - @JvmField val dirtyPathCount: Int, -) - -class FileBackedSessionInvalidationState( - private val trackedPathPredicate: (Path) -> Boolean, -) { - private val cacheLock = Any() - private val cachedFilesByPath = LinkedHashMap>() - private val dirtyPathKeys = LinkedHashSet() - private var forceFullRescan = false - - fun markDirty(changeSet: FileBackedSessionChangeSet): Int { - if (!changeSet.requiresFullRescan && changeSet.changedPaths.isEmpty()) { - return 0 - } - - var markedPaths = 0 - synchronized(cacheLock) { - forceFullRescan = forceFullRescan || changeSet.requiresFullRescan - for (path in changeSet.changedPaths) { - if (!trackedPathPredicate(path)) continue - if (dirtyPathKeys.add(toFileBackedSessionPathKey(path))) { - markedPaths++ - } - } - } - return markedPaths - } - - fun planRescan(scannedFiles: Map): FileBackedSessionRescanPlan { - val filesToParse = ArrayList() - var removedAny = false - var fullRescan = false - var dirtyPathCount = 0 - - synchronized(cacheLock) { - val iterator = cachedFilesByPath.entries.iterator() - while (iterator.hasNext()) { - if (!scannedFiles.containsKey(iterator.next().key)) { - iterator.remove() - removedAny = true - } - } - - val dirtyPathsSnapshot = dirtyPathKeys.takeIf { it.isNotEmpty() }?.let(::LinkedHashSet) - fullRescan = forceFullRescan - dirtyPathCount = dirtyPathsSnapshot?.size ?: 0 - - forceFullRescan = false - dirtyPathKeys.clear() - - for ((pathKey, stat) in scannedFiles) { - val cached = cachedFilesByPath[pathKey] - val parseBecauseDirty = dirtyPathsSnapshot?.contains(pathKey) == true - val parseBecauseStatsChanged = cached == null || cached.lastModifiedNs != stat.lastModifiedNs || cached.sizeBytes != stat.sizeBytes - if (fullRescan || parseBecauseDirty || parseBecauseStatsChanged) { - filesToParse.add(stat) - } - } - } - - return FileBackedSessionRescanPlan( - filesToParse = filesToParse, - removedAny = removedAny, - fullRescan = fullRescan, - dirtyPathCount = dirtyPathCount, - ) - } - - fun applyParsedUpdates(parsedUpdates: Map>) { - if (parsedUpdates.isEmpty()) return - - synchronized(cacheLock) { - cachedFilesByPath.putAll(parsedUpdates) - } - } - - fun snapshotCachedFiles(): Map> { - return synchronized(cacheLock) { - LinkedHashMap(cachedFilesByPath) - } - } - - fun clear() { - synchronized(cacheLock) { - cachedFilesByPath.clear() - dirtyPathKeys.clear() - forceFullRescan = false - } - } -} diff --git a/plugins/agent-workbench/lib-agent/json/src/filebacked/FileBackedSessionUpdatesFlow.kt b/plugins/agent-workbench/lib-agent/json/src/filebacked/FileBackedSessionUpdatesFlow.kt deleted file mode 100644 index 70ec972c61a6..000000000000 --- a/plugins/agent-workbench/lib-agent/json/src/filebacked/FileBackedSessionUpdatesFlow.kt +++ /dev/null @@ -1,51 +0,0 @@ -// 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.json.filebacked - -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.diagnostic.debug -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow - -fun createFileBackedSessionChangeFlow( - logger: Logger, - watcherName: String, - initContext: (() -> String)? = null, - emitInitialRefreshPing: Boolean = false, - createWatcher: (CoroutineScope, onChange: (FileBackedSessionChangeSet) -> Unit) -> AutoCloseable?, -): Flow = callbackFlow { - val context = initContext?.invoke()?.takeUnless { it.isBlank() } - logger.debug { - "Initializing $watcherName updates watcher${context?.let { " ($it)" } ?: ""}" - } - - val watcher = runCatching { - createWatcher(this) { changeSet -> - logger.debug { - "$watcherName watcher signaled change; emitting update (fullRescan=${changeSet.requiresFullRescan}, changedPaths=${changeSet.changedPaths.size})" - } - trySend(changeSet) - } - }.onFailure { t -> - logger.warn("Failed to initialize $watcherName watcher", t) - }.getOrNull() - - if (watcher == null) { - logger.debug { "$watcherName updates watcher was not initialized; updates flow will stay idle" } - awaitClose { } - return@callbackFlow - } - - if (emitInitialRefreshPing) { - val initialRefreshEmitted = trySend(FileBackedSessionChangeSet()).isSuccess - logger.debug { - "$watcherName watcher initialized; initial refresh ping emitted=$initialRefreshEmitted" - } - } - - awaitClose { - logger.debug { "Closing $watcherName updates watcher" } - watcher.close() - } -} diff --git a/plugins/agent-workbench/lib-agent/json/src/filebacked/FileBackedSessionWatcher.kt b/plugins/agent-workbench/lib-agent/json/src/filebacked/FileBackedSessionWatcher.kt deleted file mode 100644 index eb4cbde268e1..000000000000 --- a/plugins/agent-workbench/lib-agent/json/src/filebacked/FileBackedSessionWatcher.kt +++ /dev/null @@ -1,78 +0,0 @@ -// 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.json.filebacked - -import com.intellij.platform.ai.agent.filewatch.AgentWorkbenchDirectoryWatcher -import com.intellij.platform.ai.agent.filewatch.AgentWorkbenchWatchEvent -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.diagnostic.debug -import kotlinx.coroutines.CoroutineScope -import java.nio.file.Files -import java.nio.file.Path - -class FileBackedSessionWatcherSpec( - val roots: Collection, - val eventToChangeSet: (AgentWorkbenchWatchEvent) -> FileBackedSessionChangeSet?, -) - -class FileBackedSessionWatcher( - private val logger: Logger, - private val watcherName: String, - spec: FileBackedSessionWatcherSpec, - scope: CoroutineScope, - private val onChange: (FileBackedSessionChangeSet) -> Unit, - private val failureMessage: String = "$watcherName watcher failed", -) : AutoCloseable { - private val watcher: AgentWorkbenchDirectoryWatcher? - - init { - val roots = spec.roots.asSequence() - .map(::normalizeFileBackedSessionPath) - .filter { path -> Files.isDirectory(path) } - .distinct() - .toList() - logger.debug { "Registering $watcherName watcher (roots=$roots)" } - - watcher = if (roots.isEmpty()) { - logger.debug { "No watcher roots found; $watcherName watcher will stay idle" } - null - } - else { - AgentWorkbenchDirectoryWatcher( - roots = roots, - scope = scope, - onWatchEvent = { event -> handleWatchEvent(event, spec.eventToChangeSet) }, - onFailure = { t -> - logger.warn(failureMessage, t) - }, - ) - } - - if (watcher != null) { - logger.debug { "Initialized directory watcher for roots=$roots" } - } - } - - override fun close() { - logger.debug { "Closing $watcherName watcher" } - watcher?.close() - } - - private fun handleWatchEvent( - event: AgentWorkbenchWatchEvent, - eventToChangeSet: (AgentWorkbenchWatchEvent) -> FileBackedSessionChangeSet?, - ) { - val eventPath = event.path - val rootPath = event.rootPath - val eventType = event.eventType - val isDirectory = event.isDirectory - logger.debug { - "$watcherName watcher event type=$eventType path=$eventPath root=$rootPath isDirectory=$isDirectory count=${event.count}" - } - - val changeSet = eventToChangeSet(event) ?: return - logger.debug { - "$watcherName watcher detected relevant changes; notifying listeners (fullRescan=${changeSet.requiresFullRescan}, changedPaths=${changeSet.changedPaths.size})" - } - onChange(changeSet) - } -} diff --git a/plugins/agent-workbench/lib-agent/json/testSrc/filebacked/FileBackedSessionInvalidationStateTest.kt b/plugins/agent-workbench/lib-agent/json/testSrc/filebacked/FileBackedSessionInvalidationStateTest.kt deleted file mode 100644 index 6d1ae40a1115..000000000000 --- a/plugins/agent-workbench/lib-agent/json/testSrc/filebacked/FileBackedSessionInvalidationStateTest.kt +++ /dev/null @@ -1,121 +0,0 @@ -// 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.json.filebacked - -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.Timeout -import java.util.concurrent.TimeUnit -import org.junit.jupiter.api.io.TempDir -import java.nio.file.Path - -@Timeout(value = 2, unit = TimeUnit.MINUTES) -class FileBackedSessionInvalidationStateTest { - @TempDir - lateinit var tempDir: Path - - @Test - fun refreshPingDoesNotMarkDirtyPaths() { - val state = newState() - val sessionPath = tempDir.resolve("refresh/session.jsonl") - val stat = stat(sessionPath) - state.applyParsedUpdates(mapOf(stat.pathKey to cachedFile(stat, parsedValue = "initial"))) - - val markedPaths = state.markDirty(FileBackedSessionChangeSet()) - val plan = state.planRescan(mapOf(stat.pathKey to stat)) - - assertThat(markedPaths).isZero() - assertThat(plan.filesToParse).isEmpty() - assertThat(plan.dirtyPathCount).isZero() - assertThat(plan.fullRescan).isFalse() - } - - @Test - fun dirtyPathForcesReparseWhenFileStatsDidNotChange() { - val state = newState() - val sessionPath = tempDir.resolve("dirty/session.jsonl") - val stat = stat(sessionPath) - state.applyParsedUpdates(mapOf(stat.pathKey to cachedFile(stat, parsedValue = "before"))) - - val markedPaths = state.markDirty(FileBackedSessionChangeSet(changedPaths = setOf(sessionPath))) - val plan = state.planRescan(mapOf(stat.pathKey to stat)) - - assertThat(markedPaths).isEqualTo(1) - assertThat(plan.filesToParse).containsExactly(stat) - assertThat(plan.dirtyPathCount).isEqualTo(1) - assertThat(plan.fullRescan).isFalse() - } - - @Test - fun fullRescanReparsesAllScannedFiles() { - val state = newState() - val firstStat = stat(tempDir.resolve("full/first.jsonl"), lastModifiedNs = 1L) - val secondStat = stat(tempDir.resolve("full/second.jsonl"), lastModifiedNs = 2L) - state.applyParsedUpdates( - mapOf( - firstStat.pathKey to cachedFile(firstStat, parsedValue = "first"), - secondStat.pathKey to cachedFile(secondStat, parsedValue = "second"), - ) - ) - - state.markDirty(FileBackedSessionChangeSet(requiresFullRescan = true)) - val plan = state.planRescan( - linkedMapOf( - firstStat.pathKey to firstStat, - secondStat.pathKey to secondStat, - ) - ) - - assertThat(plan.filesToParse).containsExactly(firstStat, secondStat) - assertThat(plan.fullRescan).isTrue() - } - - @Test - fun removedFilesArePrunedFromCache() { - val state = newState() - val keptStat = stat(tempDir.resolve("prune/kept.jsonl"), lastModifiedNs = 1L) - val removedStat = stat(tempDir.resolve("prune/removed.jsonl"), lastModifiedNs = 2L) - state.applyParsedUpdates( - mapOf( - keptStat.pathKey to cachedFile(keptStat, parsedValue = "kept"), - removedStat.pathKey to cachedFile(removedStat, parsedValue = "removed"), - ) - ) - - val plan = state.planRescan(mapOf(keptStat.pathKey to keptStat)) - val cachedFiles = state.snapshotCachedFiles() - - assertThat(plan.removedAny).isTrue() - assertThat(plan.filesToParse).isEmpty() - assertThat(cachedFiles).containsOnlyKeys(keptStat.pathKey) - } -} - -private fun newState(): FileBackedSessionInvalidationState { - return FileBackedSessionInvalidationState { path -> - path.fileName?.toString()?.endsWith(".jsonl") == true - } -} - -private fun stat( - path: Path, - lastModifiedNs: Long = 1L, - sizeBytes: Long = 100L, -): FileBackedSessionFileStat { - return FileBackedSessionFileStat( - pathKey = toFileBackedSessionPathKey(path), - path = normalizeFileBackedSessionPath(path), - lastModifiedNs = lastModifiedNs, - sizeBytes = sizeBytes, - ) -} - -private fun cachedFile( - stat: FileBackedSessionFileStat, - parsedValue: String, -): FileBackedSessionCachedFile { - return FileBackedSessionCachedFile( - lastModifiedNs = stat.lastModifiedNs, - sizeBytes = stat.sizeBytes, - parsedValue = parsedValue, - ) -} diff --git a/plugins/agent-workbench/lib-agent/json/testSrc/filebacked/FileBackedSessionUpdatesFlowTest.kt b/plugins/agent-workbench/lib-agent/json/testSrc/filebacked/FileBackedSessionUpdatesFlowTest.kt deleted file mode 100644 index 06d3a1d442b8..000000000000 --- a/plugins/agent-workbench/lib-agent/json/testSrc/filebacked/FileBackedSessionUpdatesFlowTest.kt +++ /dev/null @@ -1,73 +0,0 @@ -// 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.json.filebacked - -import com.intellij.openapi.diagnostic.logger -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeout -import kotlinx.coroutines.withTimeoutOrNull -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.Timeout -import java.util.concurrent.TimeUnit -import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.seconds - -@Timeout(value = 2, unit = TimeUnit.MINUTES) -class FileBackedSessionUpdatesFlowTest { - @Test - fun staysIdleWhenWatcherWasNotInitialized() = runBlocking(Dispatchers.Default) { - val update = withTimeoutOrNull(1.seconds) { - createFileBackedSessionChangeFlow( - logger = logger(), - watcherName = "test watcher", - ) { _, _ -> null }.first() - } - - assertThat(update).isNull() - } - - @Test - fun emitsInitialRefreshPingWhenConfigured() { - runBlocking(Dispatchers.Default) { - val update = withTimeout(5.seconds) { - createFileBackedSessionChangeFlow( - logger = logger(), - watcherName = "test watcher", - emitInitialRefreshPing = true, - ) { _, _ -> AutoCloseable { } }.first() - } - - assertThat(update).isEqualTo(FileBackedSessionChangeSet()) - } - } - - @Test - fun closesWatcherWhenCollectorStops() = runBlocking(Dispatchers.Default) { - val closed = CompletableDeferred() - val updatesJob = launch { - createFileBackedSessionChangeFlow( - logger = logger(), - watcherName = "test watcher", - ) { _, _ -> AutoCloseable { closed.complete(Unit) } }.collect { } - } - - try { - delay(100.milliseconds) - updatesJob.cancelAndJoin() - withTimeout(5.seconds) { - closed.await() - } - } - finally { - if (updatesJob.isActive) { - updatesJob.cancelAndJoin() - } - } - } -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/BUILD.bazel b/plugins/agent-workbench/lib-agent/sessions-core/BUILD.bazel deleted file mode 100644 index 289f5b0ca27e..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/BUILD.bazel +++ /dev/null @@ -1,74 +0,0 @@ -load("@rules_jvm//:jvm.bzl", "jvm_library") - -### auto-generated section `build intellij.platform.ai.agent.sessions.core` start - -jvm_library( - name = "ai-agent-sessions-core", - srcs = glob( - [ - "src/**/*.kt", - "src/**/*.java", - "src/**/*.form", - ], - allow_empty = True, - ), - module_name = "intellij.platform.ai.agent.sessions.core", - 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/core-ui", - "//platform/platform-util-io:ide-util-io", - "//platform/projectModel-api:projectModel", - "//platform/util", - "//platform/util-ex", - "//platform/util:util-ui", - "//plugins/agent-workbench/lib-agent/core", - "//plugins/agent-workbench/prompt/core", - "//libraries/jackson3/jackson:libraries-jackson3", - "//plugins/agent-workbench/lib-agent/json", - ], -) - -jvm_library( - name = "ai-agent-sessions-core_test_lib", - testonly = True, - srcs = glob( - [], - allow_empty = True, - ), - module_name = "intellij.platform.ai.agent.sessions.core", - visibility = ["//visibility:public"], - runtime_deps = [ - # do not sort, - ":ai-agent-sessions-core", - "//libraries/kotlinx/serialization/core:core_test_lib", - "//platform/core-api:core_test_lib", - "//platform/core-ui:core-ui_test_lib", - "//platform/platform-util-io:ide-util-io_test_lib", - "//platform/projectModel-api:projectModel_test_lib", - "//platform/util:util_test_lib", - "//platform/util-ex:util-ex_test_lib", - "//platform/util:util-ui_test_lib", - "//plugins/agent-workbench/lib-agent/core:core_test_lib", - "//plugins/agent-workbench/prompt/core:core_test_lib", - "//libraries/jackson3/jackson:libraries-jackson3_test_lib", - "//plugins/agent-workbench/lib-agent/json:json_test_lib", - ], -) -### auto-generated section `build intellij.platform.ai.agent.sessions.core` end - -### auto-generated section `iml intellij.platform.ai.agent.sessions.core` start -exports_files( - [ - "intellij.platform.ai.agent.sessions.core.iml", - ], - visibility = ["//visibility:public"], -) -### auto-generated section `iml intellij.platform.ai.agent.sessions.core` end diff --git a/plugins/agent-workbench/lib-agent/sessions-core/api-dump.txt b/plugins/agent-workbench/lib-agent/sessions-core/api-dump.txt deleted file mode 100644 index 87f1609c232e..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/api-dump.txt +++ /dev/null @@ -1,740 +0,0 @@ -f:com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadActivityPresentationUpdate -- f:activityReport:com.intellij.platform.ai.agent.core.AgentThreadActivityReport -- 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.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 -- f:component2():java.lang.String -- f:component3():com.intellij.platform.ai.agent.core.AgentThreadActivityReport -- f:component4():java.lang.Long -- f:copy(java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.AgentThreadActivityReport,java.lang.Long):com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadActivityPresentationUpdate -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadActivityPresentationUpdate,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.AgentSessionThreadActivityPresentationUpdate -- equals(java.lang.Object):Z -- hashCode():I -f:com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadIdsKt -- sf:isAgentSessionPendingThreadId(java.lang.String):Z -- sf:normalizeConcreteAgentSessionThreadId(java.lang.String):java.lang.String -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.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 -- f:component2():com.intellij.platform.ai.agent.core.AgentThreadActivityReport -- f:component3():java.lang.Long -- 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 -- hashCode():I -f:com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationChangeSet -- sf:Companion:com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationChangeSet$Companion -- f:changedKeys:java.util.Set -- f:removedKeys:java.util.Set -- (java.util.Set,java.util.Set):V -- f:component1():java.util.Set -- f:component2():java.util.Set -- f:copy(java.util.Set,java.util.Set):com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationChangeSet -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationChangeSet,java.util.Set,java.util.Set,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationChangeSet -- equals(java.lang.Object):Z -- hashCode():I -- f:isEmpty():Z -f:com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationChangeSet$Companion -- f:getEMPTY():com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationChangeSet -f:com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationKey -- sf:Companion:com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationKey$Companion -- f:normalizedProjectPath:java.lang.String -- f:threadId:java.lang.String -- f:component1():java.lang.String -- f:component2-dBzBBrw():java.lang.String -- f:component3():java.lang.String -- f:copy-vFUIKBM(java.lang.String,java.lang.String,java.lang.String):com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationKey -- bs:copy-vFUIKBM$default(com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationKey,java.lang.String,java.lang.String,java.lang.String,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationKey -- equals(java.lang.Object):Z -- f:getProvider-dBzBBrw():java.lang.String -- hashCode():I -f:com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationKey$Companion -- f:create-vFUIKBM(java.lang.String,java.lang.String,java.lang.String):com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationKey -f:com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationKt -- sf:formatAgentSessionRelativeTimeShort(J,J,java.lang.String,java.lang.String):java.lang.String -- sf:formatAgentSessionThreadTitle(java.lang.String,java.lang.String,kotlin.jvm.functions.Function1):java.lang.String -- sf:formatCompactAgentSessionThreadTitle(java.lang.String,java.lang.String,kotlin.jvm.functions.Function1,I):java.lang.String -- bs:formatCompactAgentSessionThreadTitle$default(java.lang.String,java.lang.String,kotlin.jvm.functions.Function1,I,I,java.lang.Object):java.lang.String -- sf:formatCompactAgentSessionTitle(java.lang.String,I):java.lang.String -- bs:formatCompactAgentSessionTitle$default(java.lang.String,I,I,java.lang.Object):java.lang.String -- sf:normalizeAgentSessionTitle(java.lang.String):java.lang.String -f:com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationModel -- ():V -- f:clearForTests():V -- f:getChanges():kotlinx.coroutines.flow.SharedFlow -- f:getState():kotlinx.coroutines.flow.StateFlow -- f:remove(java.util.Set):com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationChangeSet -- f:replaceForTests(java.util.Map):V -- f:resolve(com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationKey):com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentation -- f:snapshot():java.util.Map -- 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-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 -- f:threadId:java.lang.String -- f:title:java.lang.String -- f:updatedAt:java.lang.Long -- (java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.AgentThreadActivityReport,java.lang.Long):V -- b:(java.lang.String,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 -- f:component2():java.lang.String -- f:component3():java.lang.String -- f:component4():com.intellij.platform.ai.agent.core.AgentThreadActivityReport -- f:component5():java.lang.Long -- f:copy(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.AgentSessionThreadPresentationPatchUpdate -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadPresentationPatchUpdate,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.AgentSessionThreadPresentationPatchUpdate -- equals(java.lang.Object):Z -- hashCode():I -com.intellij.platform.ai.agent.sessions.core.SessionActionTarget -f:com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$MoreProjects -- com.intellij.platform.ai.agent.sessions.core.SessionActionTarget -- f:hiddenCount:I -- (I):V -- f:component1():I -- f:copy(I):com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$MoreProjects -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$MoreProjects,I,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$MoreProjects -- equals(java.lang.Object):Z -- hashCode():I -f:com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$MoreThreads -- com.intellij.platform.ai.agent.sessions.core.SessionActionTarget -- f:hiddenCount:java.lang.Integer -- f:path:java.lang.String -- (java.lang.String,java.lang.Integer):V -- f:component1():java.lang.String -- f:component2():java.lang.Integer -- f:copy(java.lang.String,java.lang.Integer):com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$MoreThreads -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$MoreThreads,java.lang.String,java.lang.Integer,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$MoreThreads -- equals(java.lang.Object):Z -- hashCode():I -f:com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$Project -- com.intellij.platform.ai.agent.sessions.core.SessionActionTarget -- f:isOpen:Z -- f:path:java.lang.String -- (java.lang.String,Z):V -- f:component1():java.lang.String -- f:component2():Z -- f:copy(java.lang.String,Z):com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$Project -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$Project,java.lang.String,Z,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$Project -- equals(java.lang.Object):Z -- hashCode():I -f:com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$SubAgent -- com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$ThreadTarget -- f:parentThreadId:java.lang.String -- f:subAgent:com.intellij.platform.ai.agent.core.session.AgentSubAgent -- f:subAgentId:java.lang.String -- f:thread:com.intellij.platform.ai.agent.core.session.AgentSessionThread -- b:(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionThread,com.intellij.platform.ai.agent.core.session.AgentSubAgent,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- f:component1():java.lang.String -- f:component2-dBzBBrw():java.lang.String -- f:component3():java.lang.String -- f:component4():java.lang.String -- f:component5():java.lang.String -- f:component6():com.intellij.platform.ai.agent.core.session.AgentSessionThread -- f:component7():com.intellij.platform.ai.agent.core.session.AgentSubAgent -- f:copy-AlqNjB4(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionThread,com.intellij.platform.ai.agent.core.session.AgentSubAgent):com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$SubAgent -- bs:copy-AlqNjB4$default(com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$SubAgent,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionThread,com.intellij.platform.ai.agent.core.session.AgentSubAgent,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$SubAgent -- equals(java.lang.Object):Z -- getPath():java.lang.String -- getProvider-dBzBBrw():java.lang.String -- getThreadId():java.lang.String -- getTitle():java.lang.String -- hashCode():I -f:com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$Thread -- com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$ThreadTarget -- 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 -- f:component1():java.lang.String -- f:component2-dBzBBrw():java.lang.String -- f:component3():java.lang.String -- f:component4():java.lang.String -- f:component5():com.intellij.platform.ai.agent.core.session.AgentSessionThread -- f:copy-TrybPGk(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionThread):com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$Thread -- bs:copy-TrybPGk$default(com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$Thread,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionThread,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$Thread -- equals(java.lang.Object):Z -- getPath():java.lang.String -- getProvider-dBzBBrw():java.lang.String -- f:getThread():com.intellij.platform.ai.agent.core.session.AgentSessionThread -- getThreadId():java.lang.String -- getTitle():java.lang.String -- hashCode():I -com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$ThreadTarget -- com.intellij.platform.ai.agent.sessions.core.SessionActionTarget -- a:getPath():java.lang.String -- a:getProvider-dBzBBrw():java.lang.String -- a:getThreadId():java.lang.String -- a:getTitle():java.lang.String -f:com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$Worktree -- com.intellij.platform.ai.agent.sessions.core.SessionActionTarget -- f:path:java.lang.String -- (java.lang.String):V -- f:component1():java.lang.String -- f:copy(java.lang.String):com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$Worktree -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$Worktree,java.lang.String,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.SessionActionTarget$Worktree -- equals(java.lang.Object):Z -- hashCode():I -com.intellij.platform.ai.agent.sessions.core.config.AgentWorkbenchProjectRuntimeConfigProvider -- a:isRefreshVfsOnStatusUpdatesEnabled(java.lang.String):Z -f:com.intellij.platform.ai.agent.sessions.core.config.AgentWorkbenchProjectRuntimeConfigs -- sf:INSTANCE:com.intellij.platform.ai.agent.sessions.core.config.AgentWorkbenchProjectRuntimeConfigs -- f:isRefreshVfsOnStatusUpdatesEnabled(java.lang.String):Z -com.intellij.platform.ai.agent.sessions.core.cost.AgentSessionUsageCostCalculator -- a:calculateCost(com.intellij.platform.ai.agent.sessions.core.cost.AgentSessionUsageSnapshot):com.intellij.platform.ai.agent.core.session.AgentSessionCost -f:com.intellij.platform.ai.agent.sessions.core.cost.AgentSessionUsageCostCalculators -- sf:INSTANCE:com.intellij.platform.ai.agent.sessions.core.cost.AgentSessionUsageCostCalculators -- f:calculateCost(com.intellij.platform.ai.agent.sessions.core.cost.AgentSessionUsageSnapshot):com.intellij.platform.ai.agent.core.session.AgentSessionCost -- f:getEP_NAME():com.intellij.openapi.extensions.ExtensionPointName -f:com.intellij.platform.ai.agent.sessions.core.cost.AgentSessionUsageSnapshot -- f:cacheReadTokens:J -- f:cacheWrite1hTokens:J -- f:cacheWrite5mTokens:J -- f:cacheWriteTokens:J -- f:inputTokens:J -- f:modelId:java.lang.String -- f:nativeExactCostUsd:java.math.BigDecimal -- f:outputTokens:J -- f:reasoningTokens:J -- f:requestCount:J -- (java.lang.String,J,J,J,J,J,J,J,J,java.math.BigDecimal):V -- b:(java.lang.String,J,J,J,J,J,J,J,J,java.math.BigDecimal,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- f:component1():java.lang.String -- f:component10():java.math.BigDecimal -- f:component2():J -- f:component3():J -- f:component4():J -- f:component5():J -- f:component6():J -- f:component7():J -- f:component8():J -- f:component9():J -- f:copy(java.lang.String,J,J,J,J,J,J,J,J,java.math.BigDecimal):com.intellij.platform.ai.agent.sessions.core.cost.AgentSessionUsageSnapshot -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.cost.AgentSessionUsageSnapshot,java.lang.String,J,J,J,J,J,J,J,J,java.math.BigDecimal,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.cost.AgentSessionUsageSnapshot -- equals(java.lang.Object):Z -- hashCode():I -f:com.intellij.platform.ai.agent.sessions.core.cost.AgentSessionUsageSnapshotKt -- sf:aggregateAgentSessionUsageCost(java.util.List,kotlin.jvm.functions.Function1):com.intellij.platform.ai.agent.core.session.AgentSessionCost -com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchContributor -- a:contribute-rVRrr4o(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,kotlin.coroutines.Continuation):java.lang.Object -f:com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchContributors -- sf:INSTANCE:com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchContributors -- f:applyAll-rVRrr4o(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,kotlin.coroutines.Continuation):java.lang.Object -- bs:applyAll-rVRrr4o$default(com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchContributors,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,kotlin.coroutines.Continuation,I,java.lang.Object):java.lang.Object -f:com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchIntent -- f:generationSettings:com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings -- f:launchMode:com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode -- f:launchTargetId:java.lang.String -- f:operation:com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchOperation -- f:projectDirectory:java.lang.String -- f:projectPath:java.lang.String -- f:sessionId:java.lang.String -- b:(java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchOperation,java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode,java.lang.String,java.lang.String,com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- f:component1():java.lang.String -- f:component2():java.lang.String -- f:component3-dBzBBrw():java.lang.String -- f:component4():com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchOperation -- f:component5():java.lang.String -- f:component6():com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode -- f:component7():java.lang.String -- f:component8-fi09RKo():java.lang.String -- f:component9():com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings -- f:copy-QUbC3BQ(java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchOperation,java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode,java.lang.String,java.lang.String,com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings):com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchIntent -- bs:copy-QUbC3BQ$default(com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchIntent,java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchOperation,java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode,java.lang.String,java.lang.String,com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchIntent -- equals(java.lang.Object):Z -- f:getProvider-dBzBBrw():java.lang.String -- f:getSurfaceId-fi09RKo():java.lang.String -- hashCode():I -e:com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchOperation -- java.lang.Enum -- sf:NEW:com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchOperation -- sf:RESUME:com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchOperation -- s:getEntries():kotlin.enums.EnumEntries -- s:valueOf(java.lang.String):com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchOperation -- s:values():com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchOperation[] -f:com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchPlanner -- sf:INSTANCE:com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchPlanner -- f:plan(com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchIntent,com.intellij.openapi.project.Project,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessagePlan,java.util.List,java.util.Map,java.util.List,kotlin.jvm.functions.Function4,kotlin.coroutines.Continuation):java.lang.Object -- bs:plan$default(com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchPlanner,com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchIntent,com.intellij.openapi.project.Project,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessagePlan,java.util.List,java.util.Map,java.util.List,kotlin.jvm.functions.Function4,kotlin.coroutines.Continuation,I,java.lang.Object):java.lang.Object -com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchSpecAugmenter -- a:augment-tFgkW8w(java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,kotlin.coroutines.Continuation):java.lang.Object -f:com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchSpecAugmenters -- sf:INSTANCE:com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchSpecAugmenters -- f:augment-tFgkW8w(java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,kotlin.coroutines.Continuation):java.lang.Object -- bs:augment-tFgkW8w$default(com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchSpecAugmenters,java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,kotlin.coroutines.Continuation,I,java.lang.Object):java.lang.Object -- f:withAugmenterForTest(com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchSpecAugmenter,kotlin.jvm.functions.Function1,kotlin.coroutines.Continuation):java.lang.Object -f:com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchSpecs -- sf:INSTANCE:com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchSpecs -- f:augment-tFgkW8w(java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,kotlin.coroutines.Continuation):java.lang.Object -- bs:augment-tFgkW8w$default(com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchSpecs,java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,kotlin.coroutines.Continuation,I,java.lang.Object):java.lang.Object -f:com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchSpecsKt -- sf:insertArgumentsBefore(java.util.List,java.util.List,java.util.Set):java.util.List -- sf:removeOptions(java.util.List,java.util.Set,java.util.Set):java.util.List -- bs:removeOptions$default(java.util.List,java.util.Set,java.util.Set,I,java.lang.Object):java.util.List -- sf:replaceOrAddOption(java.util.List,java.lang.String,java.lang.String,java.util.Set):java.util.List -- bs:replaceOrAddOption$default(java.util.List,java.lang.String,java.lang.String,java.util.Set,I,java.lang.Object):java.util.List -com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionOutOfBandLaunch -- sf:Companion:com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionOutOfBandLaunch$Companion -- handles(com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionOutOfBandLaunchContext):Z -- a:handles-e2Q3-TE(java.lang.String):Z -- a:launch(com.intellij.openapi.project.Project,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionOutOfBandLaunchContext,java.lang.String,kotlin.coroutines.Continuation):java.lang.Object -f:com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionOutOfBandLaunch$Companion -- f:forContext(com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionOutOfBandLaunchContext):com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionOutOfBandLaunch -- f:forProvider-e2Q3-TE(java.lang.String):com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionOutOfBandLaunch -f:com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionOutOfBandLaunchContext -- f:generationSettings:com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings -- f:launchMode:com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode -- f:launchProfileId:java.lang.String -- f:launchTargetId:java.lang.String -- f:component1-dBzBBrw():java.lang.String -- f:component2():com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode -- f:component3():java.lang.String -- f:component4():java.lang.String -- f:component5-CYdv268():java.lang.String -- f:component6():com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings -- f:copy-NwT8XtU(java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode,java.lang.String,java.lang.String,java.lang.String,com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings):com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionOutOfBandLaunchContext -- bs:copy-NwT8XtU$default(com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionOutOfBandLaunchContext,java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode,java.lang.String,java.lang.String,java.lang.String,com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionOutOfBandLaunchContext -- equals(java.lang.Object):Z -- f:getProvider-dBzBBrw():java.lang.String -- f:getSurfaceId-CYdv268():java.lang.String -- hashCode():I -f:com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionPlannedLaunch -- f:baseLaunchSpec:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec -- f:generationModelCatalog:java.util.List -- f:intent:com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchIntent -- f:launchSpec:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec -- (com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchIntent,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,java.util.List):V -- f:component1():com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchIntent -- f:component2():com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec -- f:component3():com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec -- f:component4():java.util.List -- f:copy(com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchIntent,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,java.util.List):com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionPlannedLaunch -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionPlannedLaunch,com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchIntent,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,java.util.List,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionPlannedLaunch -- equals(java.lang.Object):Z -- hashCode():I -f:com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionThreadViewOpenPlan -- f:initialMessageDispatchPlan:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryPlan -- f:launchSpec:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec -- f:runtimeThreadId:java.lang.String -- f:subAgentId:java.lang.String -- f:threadIdentity:java.lang.String -- f:threadTitle:java.lang.String -- (java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryPlan):V -- b:(java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryPlan,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- f:component1():java.lang.String -- f:component2():com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec -- f:component3():java.lang.String -- f:component4():java.lang.String -- f:component5():java.lang.String -- f:component6():com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryPlan -- f:copy(java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryPlan):com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionThreadViewOpenPlan -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionThreadViewOpenPlan,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryPlan,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionThreadViewOpenPlan -- equals(java.lang.Object):Z -- hashCode():I -f:com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionThreadViewOpenPlanKt -- sf:resolveAgentSessionThreadViewOpenPlan-e88xiWE(java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionThread,com.intellij.platform.ai.agent.core.session.AgentSubAgent,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode,java.lang.String,java.lang.String,com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings,com.intellij.openapi.project.Project,kotlin.jvm.functions.Function4,kotlin.coroutines.Continuation):java.lang.Object -- bs:resolveAgentSessionThreadViewOpenPlan-e88xiWE$default(java.lang.String,java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionThread,com.intellij.platform.ai.agent.core.session.AgentSubAgent,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode,java.lang.String,java.lang.String,com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings,com.intellij.openapi.project.Project,kotlin.jvm.functions.Function4,kotlin.coroutines.Continuation,I,java.lang.Object):java.lang.Object -f:com.intellij.platform.ai.agent.sessions.core.launch.AwbMcpConfig -- (java.util.Set,kotlin.jvm.functions.Function1):V -- f:component1():java.util.Set -- f:component2():kotlin.jvm.functions.Function1 -- f:copy(java.util.Set,kotlin.jvm.functions.Function1):com.intellij.platform.ai.agent.sessions.core.launch.AwbMcpConfig -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.launch.AwbMcpConfig,java.util.Set,kotlin.jvm.functions.Function1,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.launch.AwbMcpConfig -- equals(java.lang.Object):Z -- f:getBuildCliArgs():kotlin.jvm.functions.Function1 -- f:getMcpServerNames():java.util.Set -- hashCode():I -f:com.intellij.platform.ai.agent.sessions.core.launch.AwbMcpConfigBuilder -- sf:AWB_DIR:java.lang.String -- sf:INSTANCE:com.intellij.platform.ai.agent.sessions.core.launch.AwbMcpConfigBuilder -- sf:PROJECT_PATH_ENV:java.lang.String -- f:buildForLaunch-_8s3iU4(java.nio.file.Path,java.lang.String):com.intellij.platform.ai.agent.sessions.core.launch.AwbMcpConfigBuilder$LaunchConfig -- f:configFilePath(java.nio.file.Path):java.nio.file.Path -- f:getLEGACY_FILTERED_NAMES():java.util.Set -- f:isDirectHttpEnabled():Z -- f:isEnabled-e2Q3-TE(java.lang.String):Z -- f:writeMergedConfigFile(java.nio.file.Path,java.lang.String,java.util.Set,java.util.List):java.nio.file.Path -- bs:writeMergedConfigFile$default(com.intellij.platform.ai.agent.sessions.core.launch.AwbMcpConfigBuilder,java.nio.file.Path,java.lang.String,java.util.Set,java.util.List,I,java.lang.Object):java.nio.file.Path -f:com.intellij.platform.ai.agent.sessions.core.launch.AwbMcpConfigBuilder$LaunchConfig -- (java.util.List,java.util.Map,java.nio.file.Path):V -- f:component1():java.util.List -- f:component2():java.util.Map -- f:component3():java.nio.file.Path -- f:copy(java.util.List,java.util.Map,java.nio.file.Path):com.intellij.platform.ai.agent.sessions.core.launch.AwbMcpConfigBuilder$LaunchConfig -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.launch.AwbMcpConfigBuilder$LaunchConfig,java.util.List,java.util.Map,java.nio.file.Path,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.launch.AwbMcpConfigBuilder$LaunchConfig -- equals(java.lang.Object):Z -- f:getConfigFile():java.nio.file.Path -- f:getEnvVariables():java.util.Map -- f:getExtraArgs():java.util.List -- hashCode():I -com.intellij.platform.ai.agent.sessions.core.launch.AwbMcpConfigProviderContributor -- sf:Companion:com.intellij.platform.ai.agent.sessions.core.launch.AwbMcpConfigProviderContributor$Companion -- contribute-e2Q3-TE(java.lang.String):com.intellij.platform.ai.agent.sessions.core.launch.AwbMcpConfig -- a:getFilteredServerNames():java.util.Set -f:com.intellij.platform.ai.agent.sessions.core.launch.AwbMcpConfigProviderContributor$Companion -- f:getEP():com.intellij.openapi.extensions.ExtensionPointName -com.intellij.platform.ai.agent.sessions.core.launch.McpStreamUrlProvider -- sf:Companion:com.intellij.platform.ai.agent.sessions.core.launch.McpStreamUrlProvider$Companion -- a:getStreamUrl():java.lang.String -f:com.intellij.platform.ai.agent.sessions.core.launch.McpStreamUrlProvider$Companion -- f:resolve():java.lang.String -e:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageDispatchAction -- java.lang.Enum -- sf:PROVIDER:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageDispatchAction -- sf:SEND_TEXT:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageDispatchAction -- s:getEntries():kotlin.enums.EnumEntries -- s:valueOf(java.lang.String):com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageDispatchAction -- s:values():com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageDispatchAction[] -f:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageDispatchStep -- f:action:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageDispatchAction -- f:recordsPrompt:Z -- f:text:java.lang.String -- f:timeoutPolicy:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageTimeoutPolicy -- ():V -- (java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageTimeoutPolicy,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageDispatchAction,Z):V -- b:(java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageTimeoutPolicy,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageDispatchAction,Z,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- f:component1():java.lang.String -- f:component2():com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageTimeoutPolicy -- f:component3():com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageDispatchAction -- f:component4():Z -- f:copy(java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageTimeoutPolicy,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageDispatchAction,Z):com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageDispatchStep -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageDispatchStep,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageTimeoutPolicy,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageDispatchAction,Z,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageDispatchStep -- equals(java.lang.Object):Z -- hashCode():I -- f:isDispatchable():Z -e:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageMode -- java.lang.Enum -- sf:PLAN:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageMode -- sf:STANDARD:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageMode -- s:getEntries():kotlin.enums.EnumEntries -- s:valueOf(java.lang.String):com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageMode -- s:values():com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageMode[] -f:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessagePlan -- sf:Companion:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessagePlan$Companion -- sf:EMPTY:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessagePlan -- f:message:java.lang.String -- f:mode:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageMode -- f:startupPolicy:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageStartupPolicy -- f:timeoutPolicy:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageTimeoutPolicy -- (java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageMode,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageStartupPolicy,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageTimeoutPolicy):V -- b:(java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageMode,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageStartupPolicy,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageTimeoutPolicy,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- f:component1():java.lang.String -- f:component2():com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageMode -- f:component3():com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageStartupPolicy -- f:component4():com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageTimeoutPolicy -- f:copy(java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageMode,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageStartupPolicy,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageTimeoutPolicy):com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessagePlan -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessagePlan,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageMode,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageStartupPolicy,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageTimeoutPolicy,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessagePlan -- equals(java.lang.Object):Z -- hashCode():I -f:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessagePlan$Companion -- f:composeDefault(com.intellij.agent.workbench.prompt.core.AgentPromptInitialMessageRequest):com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessagePlan -f:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageProviderDispatchRequest -- f:generationSettings:com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings -- f:message:java.lang.String -- f:mode:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageMode -- f:project:com.intellij.openapi.project.Project -- f:projectPath:java.lang.String -- f:threadId:java.lang.String -- (com.intellij.openapi.project.Project,java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageMode,com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings):V -- f:component1():com.intellij.openapi.project.Project -- f:component2():java.lang.String -- f:component3():java.lang.String -- f:component4():java.lang.String -- f:component5():com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageMode -- f:component6():com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings -- f:copy(com.intellij.openapi.project.Project,java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageMode,com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings):com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageProviderDispatchRequest -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageProviderDispatchRequest,com.intellij.openapi.project.Project,java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageMode,com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageProviderDispatchRequest -- equals(java.lang.Object):Z -- hashCode():I -e:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageStartupPolicy -- java.lang.Enum -- sf:POST_START_ONLY:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageStartupPolicy -- sf:TRY_STARTUP_COMMAND:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageStartupPolicy -- s:getEntries():kotlin.enums.EnumEntries -- s:valueOf(java.lang.String):com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageStartupPolicy -- s:values():com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageStartupPolicy[] -e:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageTimeoutPolicy -- java.lang.Enum -- sf:ALLOW_TIMEOUT_FALLBACK:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageTimeoutPolicy -- sf:REQUIRE_EXPLICIT_READINESS:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageTimeoutPolicy -- s:getEntries():kotlin.enums.EnumEntries -- s:valueOf(java.lang.String):com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageTimeoutPolicy -- s:values():com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageTimeoutPolicy[] -e:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryChannel -- java.lang.Enum -- sf:APP_SERVER:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryChannel -- sf:STARTUP_COMMAND:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryChannel -- sf:TERMINAL:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryChannel -- s:getEntries():kotlin.enums.EnumEntries -- s:valueOf(java.lang.String):com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryChannel -- s:values():com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryChannel[] -f:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryPlan -- sf:Companion:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryPlan$Companion -- sf:EMPTY:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryPlan -- f:promptRecord:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptRecord -- f:startupFallbackTerminalDispatch:com.intellij.platform.ai.agent.sessions.core.providers.AgentTerminalPromptDispatch -- f:startupLaunchSpecOverride:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec -- f:terminalDispatch:com.intellij.platform.ai.agent.sessions.core.providers.AgentTerminalPromptDispatch -- ():V -- (com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptRecord,com.intellij.platform.ai.agent.sessions.core.providers.AgentTerminalPromptDispatch,com.intellij.platform.ai.agent.sessions.core.providers.AgentTerminalPromptDispatch):V -- b:(com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptRecord,com.intellij.platform.ai.agent.sessions.core.providers.AgentTerminalPromptDispatch,com.intellij.platform.ai.agent.sessions.core.providers.AgentTerminalPromptDispatch,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- (com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,java.util.List,java.lang.String):V -- b:(com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,java.util.List,java.lang.String,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- f:component1():com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec -- f:component2():com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptRecord -- f:component3():com.intellij.platform.ai.agent.sessions.core.providers.AgentTerminalPromptDispatch -- f:component4():com.intellij.platform.ai.agent.sessions.core.providers.AgentTerminalPromptDispatch -- f:copy(com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptRecord,com.intellij.platform.ai.agent.sessions.core.providers.AgentTerminalPromptDispatch,com.intellij.platform.ai.agent.sessions.core.providers.AgentTerminalPromptDispatch):com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryPlan -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryPlan,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptRecord,com.intellij.platform.ai.agent.sessions.core.providers.AgentTerminalPromptDispatch,com.intellij.platform.ai.agent.sessions.core.providers.AgentTerminalPromptDispatch,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryPlan -- equals(java.lang.Object):Z -- f:getInitialMessageToken():java.lang.String -- f:getPostStartDispatchSteps():java.util.List -- hashCode():I -- f:withStartupDeliveryIgnored():com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryPlan -f:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryPlan$Companion -e:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryStatus -- java.lang.Enum -- sf:DELIVERED:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryStatus -- sf:PENDING:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryStatus -- s:getEntries():kotlin.enums.EnumEntries -- s:valueOf(java.lang.String):com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryStatus -- s:values():com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryStatus[] -f:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptRecord -- f:deliveryChannel:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryChannel -- f:deliveryStatus:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryStatus -- f:message:java.lang.String -- f:mode:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageMode -- f:token:java.lang.String -- (java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageMode,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryStatus,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryChannel):V -- b:(java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageMode,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryStatus,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryChannel,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- f:component1():java.lang.String -- f:component2():com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageMode -- f:component3():java.lang.String -- f:component4():com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryStatus -- f:component5():com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryChannel -- f:copy(java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageMode,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryStatus,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryChannel):com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptRecord -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptRecord,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageMode,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryStatus,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryChannel,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptRecord -- equals(java.lang.Object):Z -- hashCode():I -com.intellij.platform.ai.agent.sessions.core.providers.AgentOpenTopLevelThreadDispatchService -- a:dispatchIfPresent-RmkVQ7I(java.lang.String,java.lang.String,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryPlan,kotlin.coroutines.Continuation):java.lang.Object -f:com.intellij.platform.ai.agent.sessions.core.providers.AgentPendingSessionMetadata -- f:createdAtMs:J -- f:launchMode:java.lang.String -- (J,java.lang.String):V -- f:component1():J -- f:component2():java.lang.String -- f:copy(J,java.lang.String):com.intellij.platform.ai.agent.sessions.core.providers.AgentPendingSessionMetadata -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.providers.AgentPendingSessionMetadata,J,java.lang.String,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.providers.AgentPendingSessionMetadata -- equals(java.lang.Object):Z -- hashCode():I -f:com.intellij.platform.ai.agent.sessions.core.providers.AgentPrestartNewSessionLaunchRequest -- f:generationModelCatalog:java.util.List -- f:generationSettings:com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings -- f:initialMessagePlan:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessagePlan -- f:launchMode:com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode -- f:launchSpec:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec -- f:projectPath:java.lang.String -- (java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessagePlan,com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings,java.util.List,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec):V -- f:component1():java.lang.String -- f:component2():com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode -- f:component3():com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessagePlan -- f:component4():com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings -- f:component5():java.util.List -- f:component6():com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec -- f:copy(java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessagePlan,com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings,java.util.List,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec):com.intellij.platform.ai.agent.sessions.core.providers.AgentPrestartNewSessionLaunchRequest -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.providers.AgentPrestartNewSessionLaunchRequest,java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessagePlan,com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings,java.util.List,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.providers.AgentPrestartNewSessionLaunchRequest -- equals(java.lang.Object):Z -- hashCode():I -f:com.intellij.platform.ai.agent.sessions.core.providers.AgentPrestartedNewSessionLaunch -- f:initialMessageDispatchPlan:com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryPlan -- f:launchSpec:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec -- (com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryPlan):V -- b:(com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryPlan,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- f:component1():com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec -- f:component2():com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryPlan -- f:copy(com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryPlan):com.intellij.platform.ai.agent.sessions.core.providers.AgentPrestartedNewSessionLaunch -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.providers.AgentPrestartedNewSessionLaunch,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryPlan,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.providers.AgentPrestartedNewSessionLaunch -- equals(java.lang.Object):Z -- hashCode():I -f:com.intellij.platform.ai.agent.sessions.core.providers.AgentPromptProviderOption -- f:defaultSelected:Z -- f:enabledForExistingTask:Z -- f:enabledForNewTask:Z -- f:id:java.lang.String -- f:labelFallback:java.lang.String -- f:labelKey:java.lang.String -- (java.lang.String,java.lang.String,java.lang.String,Z,Z,Z):V -- b:(java.lang.String,java.lang.String,java.lang.String,Z,Z,Z,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():Z -- f:component6():Z -- f:copy(java.lang.String,java.lang.String,java.lang.String,Z,Z,Z):com.intellij.platform.ai.agent.sessions.core.providers.AgentPromptProviderOption -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.providers.AgentPromptProviderOption,java.lang.String,java.lang.String,java.lang.String,Z,Z,Z,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.providers.AgentPromptProviderOption -- equals(java.lang.Object):Z -- hashCode():I -f:com.intellij.platform.ai.agent.sessions.core.providers.AgentPromptProviderOptionKt -- sf:AGENT_PROMPT_PROVIDER_OPTION_PLAN_MODE:java.lang.String -- sf:getAGENT_PROMPT_PROVIDER_PLAN_MODE_OPTION():com.intellij.platform.ai.agent.sessions.core.providers.AgentPromptProviderOption -e:com.intellij.platform.ai.agent.sessions.core.providers.AgentPromptProviderOptionTarget -- java.lang.Enum -- sf:EXISTING_TASK:com.intellij.platform.ai.agent.sessions.core.providers.AgentPromptProviderOptionTarget -- sf:NEW_TASK:com.intellij.platform.ai.agent.sessions.core.providers.AgentPromptProviderOptionTarget -- s:getEntries():kotlin.enums.EnumEntries -- s:valueOf(java.lang.String):com.intellij.platform.ai.agent.sessions.core.providers.AgentPromptProviderOptionTarget -- s:values():com.intellij.platform.ai.agent.sessions.core.providers.AgentPromptProviderOptionTarget[] -f:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionLaunchProfileSnapshot -- f:builtInProfiles:java.util.List -- f:userProfiles:java.util.List -- (java.util.List,java.util.List):V -- f:component1():java.util.List -- f:component2():java.util.List -- f:copy(java.util.List,java.util.List):com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionLaunchProfileSnapshot -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionLaunchProfileSnapshot,java.util.List,java.util.List,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionLaunchProfileSnapshot -- equals(java.lang.Object):Z -- f:getAllProfiles():java.util.List -- hashCode():I -f:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionLaunchProfilesKt -- sf:BUILT_IN_LAUNCH_PROFILE_PREFIX:java.lang.String -- sf:builtInLaunchProfileId-9c6lQVA(java.lang.String,com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode):java.lang.String -- sf:effectiveLaunchProfiles(java.util.List,java.util.List):java.util.List -- sf:generationSettingsForPlanMode(com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings,Z):com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings -- sf:initialMessageRequestForLaunchProfile(com.intellij.agent.workbench.prompt.core.AgentPromptLaunchProfile):com.intellij.agent.workbench.prompt.core.AgentPromptInitialMessageRequest -- sf:launchProfileEditablePayload(com.intellij.agent.workbench.prompt.core.AgentPromptLaunchProfile):com.intellij.agent.workbench.prompt.core.AgentPromptLaunchProfile -- sf:launchProfileMatchesBuiltIn(com.intellij.agent.workbench.prompt.core.AgentPromptLaunchProfile,com.intellij.agent.workbench.prompt.core.AgentPromptLaunchProfile):Z -- sf:normalizedUserLaunchProfile(com.intellij.agent.workbench.prompt.core.AgentPromptLaunchProfile):com.intellij.agent.workbench.prompt.core.AgentPromptLaunchProfile -f:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionMenuCommand -- f:argumentHint:java.lang.String -- f:command:java.lang.String -- (java.lang.String,java.lang.String):V -- b:(java.lang.String,java.lang.String,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- f:component1():java.lang.String -- f:component2():java.lang.String -- f:copy(java.lang.String,java.lang.String):com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionMenuCommand -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionMenuCommand,java.lang.String,java.lang.String,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionMenuCommand -- equals(java.lang.Object):Z -- hashCode():I -f:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionPromptCommandCompletionEntry -- f:argumentHint:java.lang.String -- f:command:java.lang.String -- f:kind:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionPromptCommandCompletionKind -- f:sourceKey:java.lang.String -- f:sourcePath:java.nio.file.Path -- (java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionPromptCommandCompletionKind,java.lang.String,java.nio.file.Path,java.lang.String):V -- b:(java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionPromptCommandCompletionKind,java.lang.String,java.nio.file.Path,java.lang.String,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- f:component1():java.lang.String -- f:component2():com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionPromptCommandCompletionKind -- f:component3():java.lang.String -- f:component4():java.nio.file.Path -- f:component5():java.lang.String -- f:copy(java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionPromptCommandCompletionKind,java.lang.String,java.nio.file.Path,java.lang.String):com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionPromptCommandCompletionEntry -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionPromptCommandCompletionEntry,java.lang.String,com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionPromptCommandCompletionKind,java.lang.String,java.nio.file.Path,java.lang.String,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionPromptCommandCompletionEntry -- equals(java.lang.Object):Z -- hashCode():I -e:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionPromptCommandCompletionKind -- java.lang.Enum -- sf:COMMAND:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionPromptCommandCompletionKind -- sf:MENU:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionPromptCommandCompletionKind -- sf:SKILL:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionPromptCommandCompletionKind -- s:getEntries():kotlin.enums.EnumEntries -- s:valueOf(java.lang.String):com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionPromptCommandCompletionKind -- s:values():com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionPromptCommandCompletionKind[] -e:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProviderCliVisibilityPolicy -- java.lang.Enum -- sf:DISCOVER_WHEN_AVAILABLE:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProviderCliVisibilityPolicy -- sf:PROMINENT:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProviderCliVisibilityPolicy -- s:getEntries():kotlin.enums.EnumEntries -- s:valueOf(java.lang.String):com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProviderCliVisibilityPolicy -- s:values():com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProviderCliVisibilityPolicy[] -com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProviderFeatureSettings -- isProviderFeatureEnabled-9c6lQVA(java.lang.String,java.lang.String):Z -- setProviderFeatureEnabled-3n3rsyc(java.lang.String,java.lang.String,Z):V -f:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProviderFeatureSettingsExtensions -- sf:INSTANCE:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProviderFeatureSettingsExtensions -- f:getEP_NAME():com.intellij.openapi.extensions.ExtensionPointName -- f:isProviderFeatureEnabled-9c6lQVA(java.lang.String,java.lang.String):Z -- f:setProviderFeatureEnabled-3n3rsyc(java.lang.String,java.lang.String,Z):V -f:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProviderPlanModeKt -- sf:buildPlanModeInitialMessagePlan(com.intellij.agent.workbench.prompt.core.AgentPromptInitialMessageRequest,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageStartupPolicy,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageStartupPolicy):com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessagePlan -- bs:buildPlanModeInitialMessagePlan$default(com.intellij.agent.workbench.prompt.core.AgentPromptInitialMessageRequest,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageStartupPolicy,com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageStartupPolicy,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessagePlan -- sf:isBlockedForExistingThreadPlanMode(com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessagePlan,com.intellij.platform.ai.agent.core.AgentThreadActivity):Z -- sf:isBusyForExistingThreadPlanMode(com.intellij.platform.ai.agent.core.AgentThreadActivity):Z -- sf:isPlanModeRequested(com.intellij.agent.workbench.prompt.core.AgentPromptInitialMessageRequest):Z -com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProviderUiContributor -- createToolWindowNorthComponent(com.intellij.openapi.project.Project):javax.swing.JComponent -- onThreadViewOpened():V -f:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProviderUiContributorBean -- com.intellij.serviceContainer.BaseKeyedLazyInstance -- implementation:java.lang.String -- providerId:java.lang.String -- ():V -- f:matches-e2Q3-TE(java.lang.String):Z -f:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProviderUiContributors -- sf:INSTANCE:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProviderUiContributors -- f:forProvider-e2Q3-TE(java.lang.String):java.util.List -- f:getEP_NAME():com.intellij.openapi.extensions.ExtensionPointName -f:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec -- f:command:java.util.List -- f:containerSessionId:java.lang.String -- f:envVariables:java.util.Map -- f:preallocatedSessionId:java.lang.String -- f:useTerminalDefaultShell:Z -- f:workingDirectory:java.lang.String -- (java.util.List,java.util.Map,java.lang.String,Z,java.lang.String,java.lang.String):V -- b:(java.util.List,java.util.Map,java.lang.String,Z,java.lang.String,java.lang.String,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- f:component1():java.util.List -- f:component2():java.util.Map -- f:component3():java.lang.String -- f:component4():Z -- f:component5():java.lang.String -- f:component6():java.lang.String -- f:copy(java.util.List,java.util.Map,java.lang.String,Z,java.lang.String,java.lang.String):com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec,java.util.List,java.util.Map,java.lang.String,Z,java.lang.String,java.lang.String,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec -- equals(java.lang.Object):Z -- hashCode():I -f:com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalRestoreContext -- f:workingDirectory:java.lang.String -- ():V -- (java.lang.String):V -- b:(java.lang.String,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- f:component1():java.lang.String -- f:copy(java.lang.String):com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalRestoreContext -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalRestoreContext,java.lang.String,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalRestoreContext -- equals(java.lang.Object):Z -- hashCode():I -f:com.intellij.platform.ai.agent.sessions.core.providers.AgentTerminalPromptDispatch -- f:stepIndex:I -- f:steps:java.util.List -- (java.util.List,I):V -- b:(java.util.List,I,I,kotlin.jvm.internal.DefaultConstructorMarker):V -- f:component1():java.util.List -- f:component2():I -- f:copy(java.util.List,I):com.intellij.platform.ai.agent.sessions.core.providers.AgentTerminalPromptDispatch -- bs:copy$default(com.intellij.platform.ai.agent.sessions.core.providers.AgentTerminalPromptDispatch,java.util.List,I,I,java.lang.Object):com.intellij.platform.ai.agent.sessions.core.providers.AgentTerminalPromptDispatch -- equals(java.lang.Object):Z -- hashCode():I -- f:normalized():com.intellij.platform.ai.agent.sessions.core.providers.AgentTerminalPromptDispatch -f:com.intellij.platform.ai.agent.sessions.core.providers.BaseAgentSessionSourceKt$sam$i$java_util_function_BiFunction$0 -- java.util.function.BiFunction -- (kotlin.jvm.functions.Function2):V -- bf:apply(java.lang.Object,java.lang.Object):java.lang.Object -f:com.intellij.platform.ai.agent.sessions.core.util.JbCentralCliSupport -- sf:INSTANCE:com.intellij.platform.ai.agent.sessions.core.util.JbCentralCliSupport -- f:findExecutable():java.lang.String -- f:isAvailable():Z -f:com.intellij.platform.ai.agent.sessions.core.util.JbCentralCliSupportKt -- sf:AGENT_WORKBENCH_JBCENTRAL_PATH_PROPERTY:java.lang.String -f:com.intellij.platform.ai.agent.sessions.core.util.JbCentralCliSupportTestHook -- sf:INSTANCE:com.intellij.platform.ai.agent.sessions.core.util.JbCentralCliSupportTestHook -- f:pathLookupOverride():kotlin.jvm.functions.Function0 -- f:replacePathLookupForTest(kotlin.jvm.functions.Function0):kotlin.jvm.functions.Function0 diff --git a/plugins/agent-workbench/lib-agent/sessions-core/exposed-third-party-api.txt b/plugins/agent-workbench/lib-agent/sessions-core/exposed-third-party-api.txt deleted file mode 100644 index 178e673bf44d..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/exposed-third-party-api.txt +++ /dev/null @@ -1,5 +0,0 @@ -com/intellij/agent/workbench/prompt/core/AgentPromptGenerationSettings -com/intellij/agent/workbench/prompt/core/AgentPromptInitialMessageRequest -com/intellij/agent/workbench/prompt/core/AgentPromptLaunchProfile -java/math/BigDecimal -kotlin/jvm/internal/DefaultConstructorMarker diff --git a/plugins/agent-workbench/lib-agent/sessions-core/intellij.platform.ai.agent.sessions.core.iml b/plugins/agent-workbench/lib-agent/sessions-core/intellij.platform.ai.agent.sessions.core.iml deleted file mode 100644 index ff893e67235d..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/intellij.platform.ai.agent.sessions.core.iml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - $KOTLIN_BUNDLED$/lib/kotlinx-serialization-compiler-plugin.jar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/plugins/agent-workbench/lib-agent/sessions-core/resources/intellij.platform.ai.agent.sessions.core.xml b/plugins/agent-workbench/lib-agent/sessions-core/resources/intellij.platform.ai.agent.sessions.core.xml deleted file mode 100644 index 8ad7d41fe769..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/resources/intellij.platform.ai.agent.sessions.core.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/AgentSessionThreadIds.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/AgentSessionThreadIds.kt deleted file mode 100644 index 763bdb4deee6..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/AgentSessionThreadIds.kt +++ /dev/null @@ -1,13 +0,0 @@ -// 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 - -private const val PENDING_THREAD_ID_PREFIX = "new-" - -fun isAgentSessionPendingThreadId(threadId: String): Boolean { - return threadId.trim().startsWith(PENDING_THREAD_ID_PREFIX) -} - -fun normalizeConcreteAgentSessionThreadId(threadId: String): String? { - val normalizedThreadId = threadId.trim() - return normalizedThreadId.takeIf { it.isNotEmpty() && !isAgentSessionPendingThreadId(it) } -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/AgentSessionThreadPresentation.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/AgentSessionThreadPresentation.kt deleted file mode 100644 index 232b50d5d90e..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/AgentSessionThreadPresentation.kt +++ /dev/null @@ -1,90 +0,0 @@ -// 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.openapi.util.NlsSafe -import com.intellij.openapi.util.text.StringUtil -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.roundToLong - -private const val COMPACT_AGENT_SESSION_TITLE_LENGTH = 50 -private val THREAD_TITLE_WHITESPACE = Regex("\\s+") - -fun normalizeAgentSessionTitle(title: String?): @NlsSafe String? { - return title - ?.replace('\n', ' ') - ?.replace('\r', ' ') - ?.replace(THREAD_TITLE_WHITESPACE, " ") - ?.trim() - ?.takeIf { it.isNotEmpty() } -} - -fun formatCompactAgentSessionTitle( - title: String, - maxLength: Int = COMPACT_AGENT_SESSION_TITLE_LENGTH, -): @NlsSafe String { - return StringUtil.trimMiddle(title, maxLength) -} - -fun formatCompactAgentSessionThreadTitle( - threadId: String, - title: String, - fallbackTitle: (idPrefix: String) -> String, - maxLength: Int = COMPACT_AGENT_SESSION_TITLE_LENGTH, -): @NlsSafe String { - return formatCompactAgentSessionTitle( - title = formatAgentSessionThreadTitle(threadId = threadId, title = title, fallbackTitle = fallbackTitle), - maxLength = maxLength, - ) -} - -fun formatAgentSessionThreadTitle( - threadId: String, - title: String, - fallbackTitle: (idPrefix: String) -> String, -): @NlsSafe String { - val normalized = normalizeAgentSessionTitle(title) - if (normalized != null) { - return normalized - } - val idPrefix = threadId.trim().takeIf { it.isNotEmpty() }?.take(8) ?: "unknown" - return fallbackTitle(idPrefix) -} - -fun formatAgentSessionRelativeTimeShort( - timestamp: Long, - now: Long, - nowLabel: String, - unknownLabel: String, -): String { - if (timestamp <= 0L) { - return unknownLabel - } - - val absSeconds = abs(((timestamp - now) / 1000.0).roundToLong()) - if (absSeconds < 60) { - return nowLabel - } - if (absSeconds < 60 * 60) { - val value = max(1, (absSeconds / 60.0).roundToLong()) - return "${value}m" - } - if (absSeconds < 60 * 60 * 24) { - val value = max(1, (absSeconds / (60.0 * 60.0)).roundToLong()) - return "${value}h" - } - if (absSeconds < 60 * 60 * 24 * 7) { - val value = max(1, (absSeconds / (60.0 * 60.0 * 24.0)).roundToLong()) - return "${value}d" - } - if (absSeconds < 60 * 60 * 24 * 30) { - val value = max(1, (absSeconds / (60.0 * 60.0 * 24.0 * 7.0)).roundToLong()) - return "${value}w" - } - if (absSeconds < 60 * 60 * 24 * 365) { - val value = max(1, (absSeconds / (60.0 * 60.0 * 24.0 * 30.0)).roundToLong()) - return "${value}mo" - } - val value = max(1, (absSeconds / (60.0 * 60.0 * 24.0 * 365.0)).roundToLong()) - return "${value}y" -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/AgentSessionThreadPresentationModel.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/AgentSessionThreadPresentationModel.kt deleted file mode 100644 index b8bbdbb72f91..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/AgentSessionThreadPresentationModel.kt +++ /dev/null @@ -1,316 +0,0 @@ -// 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.AgentThreadActivityReport -import com.intellij.platform.ai.agent.core.normalizeAgentWorkbenchPath -import com.intellij.platform.ai.agent.core.session.AgentSessionProvider -import com.intellij.platform.ai.agent.core.session.AgentSessionThread -import com.intellij.openapi.components.Service -import com.intellij.openapi.util.NlsSafe -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import org.jetbrains.annotations.TestOnly - -data class AgentSessionThreadPresentationKey( - @JvmField val normalizedProjectPath: String, - val provider: AgentSessionProvider, - @JvmField val threadId: String, -) { - companion object { - fun create(projectPath: String, provider: AgentSessionProvider, threadId: String): AgentSessionThreadPresentationKey? { - val normalizedProjectPath = normalizeAgentWorkbenchPath(projectPath).takeIf { it.isNotBlank() } ?: return null - val normalizedThreadId = threadId.trim().takeIf { it.isNotEmpty() } ?: return null - return AgentSessionThreadPresentationKey( - normalizedProjectPath = normalizedProjectPath, - provider = provider, - threadId = normalizedThreadId, - ) - } - } -} - -/** - * Shared thread presentation for editor/tab chrome and fresh title projection. - * Tool-window activity counters must use canonical [AgentSessionThread.activityReport]. - */ -data class AgentSessionThreadPresentation( - @JvmField val title: @NlsSafe String, - @JvmField val activityReport: AgentThreadActivityReport, - @JvmField val updatedAt: Long? = null, -) - -data class AgentSessionThreadPresentationChangeSet( - @JvmField val changedKeys: Set, - @JvmField val removedKeys: Set, -) { - companion object { - val EMPTY: AgentSessionThreadPresentationChangeSet = AgentSessionThreadPresentationChangeSet( - changedKeys = emptySet(), - removedKeys = emptySet(), - ) - } - - val isEmpty: Boolean - get() = changedKeys.isEmpty() && removedKeys.isEmpty() -} - -data class AgentSessionThreadActivityPresentationUpdate( - @JvmField val path: String, - @JvmField val threadId: String, - @JvmField val activityReport: AgentThreadActivityReport, - @JvmField val updatedAt: Long? = null, -) - -@Service(Service.Level.APP) -class AgentSessionThreadPresentationModel { - private val lock = Any() - private val mutableState = MutableStateFlow>(emptyMap()) - private val mutableChanges = MutableSharedFlow(extraBufferCapacity = 64) - - val state: StateFlow> = mutableState.asStateFlow() - val changes: SharedFlow = mutableChanges.asSharedFlow() - - fun snapshot(): Map = mutableState.value - - fun resolve(key: AgentSessionThreadPresentationKey): AgentSessionThreadPresentation? = mutableState.value[key] - - fun updateThread( - path: String, - provider: AgentSessionProvider, - threadId: String, - title: String, - activityReport: AgentThreadActivityReport? = null, - updatedAt: Long? = null, - ): AgentSessionThreadPresentationChangeSet { - val key = AgentSessionThreadPresentationKey.create(projectPath = path, provider = provider, threadId = threadId) - ?: return AgentSessionThreadPresentationChangeSet.EMPTY - val normalizedTitle = normalizeAgentSessionTitle(title) ?: title - return putMerged(mapOf(key to AgentSessionThreadPresentationPatch(title = normalizedTitle, - activityReport = activityReport, - updatedAt = updatedAt))) - } - - fun updateActivityHints( - provider: AgentSessionProvider, - updates: Collection, - ): AgentSessionThreadPresentationChangeSet { - if (updates.isEmpty()) { - return AgentSessionThreadPresentationChangeSet.EMPTY - } - return updatePresentationHints( - provider = provider, - updates = updates.map { update -> - AgentSessionThreadPresentationPatchUpdate( - path = update.path, - threadId = update.threadId, - activityReport = update.activityReport, - updatedAt = update.updatedAt, - ) - }, - ) - } - - fun updatePresentationHints( - provider: AgentSessionProvider, - updates: Collection, - ): AgentSessionThreadPresentationChangeSet { - if (updates.isEmpty()) { - return AgentSessionThreadPresentationChangeSet.EMPTY - } - val inputs = LinkedHashMap(updates.size) - for ((path, threadId, title, activityReport, updatedAt) in updates) { - val key = AgentSessionThreadPresentationKey.create(projectPath = path, provider = provider, threadId = threadId) ?: continue - inputs[key] = AgentSessionThreadPresentationPatch( - title = normalizeAgentSessionTitle(title), - activityReport = activityReport, - updatedAt = updatedAt, - ) - } - return putMerged(inputs) - } - - fun updateProviderSnapshot( - provider: AgentSessionProvider, - authoritativePaths: Set, - threadsByPath: Map>, - ): AgentSessionThreadPresentationChangeSet { - if (authoritativePaths.isEmpty() && threadsByPath.isEmpty()) { - return AgentSessionThreadPresentationChangeSet.EMPTY - } - - val normalizedAuthoritativePaths = authoritativePaths - .asSequence() - .map(::normalizeAgentWorkbenchPath) - .filter(String::isNotBlank) - .toCollection(LinkedHashSet()) - val presentationsByKey = LinkedHashMap() - for ((path, threads) in threadsByPath) { - threads.forEach { thread -> - if (thread.provider != provider) return@forEach - val key = - AgentSessionThreadPresentationKey.create(projectPath = path, provider = thread.provider, threadId = thread.id) ?: return@forEach - presentationsByKey[key] = AgentSessionThreadPresentation( - title = normalizeAgentSessionTitle(thread.title) ?: thread.title, - activityReport = thread.activityReport, - updatedAt = thread.updatedAt, - ) - } - } - - return updateState { current -> - val next = LinkedHashMap(current.size + presentationsByKey.size) - val changedKeys = LinkedHashSet() - val removedKeys = LinkedHashSet() - - for ((key, presentation) in current) { - if (key.provider == provider && key.normalizedProjectPath in normalizedAuthoritativePaths && key !in presentationsByKey) { - removedKeys.add(key) - continue - } - next[key] = presentation - } - - for ((key, presentation) in presentationsByKey) { - val mergedPresentation = mergePresentation(existing = next[key], incoming = presentation) - if (next[key] == mergedPresentation) continue - next[key] = mergedPresentation - changedKeys.add(key) - } - - PresentationStateUpdate(next = next, changedKeys = changedKeys, removedKeys = removedKeys) - } - } - - fun remove(keys: Set): AgentSessionThreadPresentationChangeSet { - if (keys.isEmpty()) { - return AgentSessionThreadPresentationChangeSet.EMPTY - } - return updateState { current -> - val next = LinkedHashMap(current) - val removedKeys = LinkedHashSet() - for (key in keys) { - if (next.remove(key) != null) { - removedKeys.add(key) - } - } - PresentationStateUpdate(next = next, changedKeys = emptySet(), removedKeys = removedKeys) - } - } - - @TestOnly - fun replaceForTests(presentationsByKey: Map) { - synchronized(lock) { - mutableState.value = LinkedHashMap(presentationsByKey) - } - } - - @TestOnly - fun clearForTests() { - synchronized(lock) { - mutableState.value = emptyMap() - } - } - - private fun putMerged( - updates: Map, - ): AgentSessionThreadPresentationChangeSet { - if (updates.isEmpty()) return AgentSessionThreadPresentationChangeSet.EMPTY - return updateState { current -> - val next = LinkedHashMap(current) - val changedKeys = LinkedHashSet() - for ((key, update) in updates) { - val existing = next[key] - val presentation = mergePresentation(existing = existing, patch = update) - if (existing == presentation) continue - next[key] = presentation - changedKeys.add(key) - } - PresentationStateUpdate(next = next, changedKeys = changedKeys, removedKeys = emptySet()) - } - } - - private fun updateState(transform: (Map) -> PresentationStateUpdate): AgentSessionThreadPresentationChangeSet { - val changeSet = synchronized(lock) { - val update = transform(mutableState.value) - if (update.changedKeys.isEmpty() && update.removedKeys.isEmpty()) { - return@synchronized AgentSessionThreadPresentationChangeSet.EMPTY - } - mutableState.value = update.next - AgentSessionThreadPresentationChangeSet( - changedKeys = update.changedKeys, - removedKeys = update.removedKeys, - ) - } - if (!changeSet.isEmpty) { - mutableChanges.tryEmit(changeSet) - } - return changeSet - } -} - -data class AgentSessionThreadPresentationPatchUpdate( - @JvmField val path: String, - @JvmField val threadId: String, - @JvmField val title: String? = null, - @JvmField val activityReport: AgentThreadActivityReport? = null, - @JvmField val updatedAt: Long? = null, -) - -private fun mergePresentation( - existing: AgentSessionThreadPresentation?, - incoming: AgentSessionThreadPresentation, -): AgentSessionThreadPresentation { - return mergePresentation( - existing = existing, - patch = AgentSessionThreadPresentationPatch( - title = incoming.title, - activityReport = incoming.activityReport, - updatedAt = incoming.updatedAt, - ), - ) -} - -private fun mergePresentation( - existing: AgentSessionThreadPresentation?, - patch: AgentSessionThreadPresentationPatch, -): AgentSessionThreadPresentation { - val shouldApplyActivity = shouldApplyActivityUpdate(existingUpdatedAt = existing?.updatedAt, incomingUpdatedAt = patch.updatedAt) - return AgentSessionThreadPresentation( - title = patch.title ?: existing?.title.orEmpty(), - activityReport = if (shouldApplyActivity) { - patch.activityReport ?: existing?.activityReport ?: AgentThreadActivityReport.READY - } - else { - existing?.activityReport ?: AgentThreadActivityReport.READY - }, - updatedAt = mergeUpdatedAt(existingUpdatedAt = existing?.updatedAt, incomingUpdatedAt = patch.updatedAt), - ) -} - -private fun shouldApplyActivityUpdate(existingUpdatedAt: Long?, incomingUpdatedAt: Long?): Boolean { - return existingUpdatedAt == null || incomingUpdatedAt == null || incomingUpdatedAt >= existingUpdatedAt -} - -private fun mergeUpdatedAt(existingUpdatedAt: Long?, incomingUpdatedAt: Long?): Long? { - return when { - existingUpdatedAt == null -> incomingUpdatedAt - incomingUpdatedAt == null -> existingUpdatedAt - else -> maxOf(existingUpdatedAt, incomingUpdatedAt) - } -} - -private data class PresentationStateUpdate( - @JvmField val next: Map, - @JvmField val changedKeys: Set, - @JvmField val removedKeys: Set, -) - -private data class AgentSessionThreadPresentationPatch( - @JvmField val title: String? = null, - @JvmField val activityReport: AgentThreadActivityReport? = null, - @JvmField val updatedAt: Long? = null, -) diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/AgentSessionThreadRebindPolicy.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/AgentSessionThreadRebindPolicy.kt deleted file mode 100644 index 855b7718198a..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/AgentSessionThreadRebindPolicy.kt +++ /dev/null @@ -1,36 +0,0 @@ -// 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 org.jetbrains.annotations.ApiStatus - -@ApiStatus.Internal -object AgentSessionThreadRebindPolicy { - const val PENDING_THREAD_MATCH_PRE_WINDOW_MS: Long = 20_000L - const val PENDING_THREAD_MATCH_POST_WINDOW_MS: Long = 120_000L - const val PENDING_THREAD_NO_BASELINE_AUTO_BIND_MAX_AGE_MS: Long = PENDING_THREAD_MATCH_POST_WINDOW_MS - const val PENDING_THREAD_REFRESH_RETRY_INTERVAL_MS: Long = 2_000L - const val CONCRETE_NEW_THREAD_MATCH_POST_WINDOW_MS: Long = 30_000L - const val CONCRETE_NEW_THREAD_REBIND_MAX_AGE_MS: Long = CONCRETE_NEW_THREAD_MATCH_POST_WINDOW_MS - const val CONCRETE_NEW_THREAD_REFRESH_RETRY_INTERVAL_MS: Long = 2_000L - - fun isConcreteNewThreadRebindAnchorActive(rebindRequestedAtMs: Long, currentTimeMs: Long): Boolean { - return currentTimeMs >= rebindRequestedAtMs && - currentTimeMs - rebindRequestedAtMs < CONCRETE_NEW_THREAD_REBIND_MAX_AGE_MS - } - - fun concreteNewThreadRefreshRetryDelayMs( - rebindRequestedAtMs: Long, - currentTimeMs: Long, - retryIntervalMs: Long = CONCRETE_NEW_THREAD_REFRESH_RETRY_INTERVAL_MS, - ): Long? { - if (currentTimeMs < rebindRequestedAtMs) { - return null - } - val retryDeadlineMs = rebindRequestedAtMs + CONCRETE_NEW_THREAD_REBIND_MAX_AGE_MS - val remainingMs = retryDeadlineMs - currentTimeMs - if (remainingMs <= 0L) { - return null - } - return minOf(retryIntervalMs, remainingMs) - } -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/SessionActionTarget.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/SessionActionTarget.kt deleted file mode 100644 index 9b05a1d20b88..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/SessionActionTarget.kt +++ /dev/null @@ -1,54 +0,0 @@ -// 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.session.AgentSessionProvider -import com.intellij.platform.ai.agent.core.session.AgentSessionThread -import com.intellij.platform.ai.agent.core.session.AgentSubAgent - -sealed interface SessionActionTarget { - data class Project( - @JvmField val path: String, - @JvmField val isOpen: Boolean, - ) : SessionActionTarget - - data class Worktree( - @JvmField val path: String, - ) : SessionActionTarget - - sealed interface ThreadTarget : SessionActionTarget { - val path: String - val provider: AgentSessionProvider - val threadId: String - val title: String - } - - data class Thread( - override val path: String, - override val provider: AgentSessionProvider, - override val threadId: String, - override val title: String, - val thread: AgentSessionThread? = null, - ) : ThreadTarget - - data class SubAgent( - override val path: String, - override val provider: AgentSessionProvider, - @JvmField val parentThreadId: String, - @JvmField val subAgentId: String, - override val title: String, - @JvmField val thread: AgentSessionThread? = null, - @JvmField val subAgent: AgentSubAgent? = null, - ) : ThreadTarget { - override val threadId: String - get() = subAgentId - } - - data class MoreProjects( - @JvmField val hiddenCount: Int, - ) : SessionActionTarget - - data class MoreThreads( - @JvmField val path: String, - @JvmField val hiddenCount: Int?, - ) : SessionActionTarget -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/config/AgentWorkbenchProjectRuntimeConfigProvider.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/config/AgentWorkbenchProjectRuntimeConfigProvider.kt deleted file mode 100644 index c2307acca60b..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/config/AgentWorkbenchProjectRuntimeConfigProvider.kt +++ /dev/null @@ -1,38 +0,0 @@ -// 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.config - -import com.intellij.platform.ai.agent.core.extensions.SingleExtensionPointResolver -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.extensions.ExtensionPointName - -fun interface AgentWorkbenchProjectRuntimeConfigProvider { - fun isRefreshVfsOnStatusUpdatesEnabled(projectRoot: String): Boolean -} - -private class AgentWorkbenchProjectRuntimeConfigProviderRegistry - -private val LOG = logger() -private val AGENT_WORKBENCH_PROJECT_RUNTIME_CONFIG_PROVIDER_EP: ExtensionPointName = - ExtensionPointName("com.intellij.agent.workbench.projectRuntimeConfigProvider") - -private val REGISTERED_PROVIDER = SingleExtensionPointResolver( - log = LOG, - extensionPoint = AGENT_WORKBENCH_PROJECT_RUNTIME_CONFIG_PROVIDER_EP, - unavailableMessage = "Agent Workbench project runtime config provider EP is unavailable in this context", - multipleExtensionsMessage = { providers -> - "Multiple Agent Workbench project runtime config providers registered; using first: ${providers.map { it::class.java.name }}" - }, -) - -object AgentWorkbenchProjectRuntimeConfigs { - fun isRefreshVfsOnStatusUpdatesEnabled(projectRoot: String): Boolean { - val provider = REGISTERED_PROVIDER.findFirstOrNull() ?: return true - return try { - provider.isRefreshVfsOnStatusUpdatesEnabled(projectRoot) - } - catch (t: Throwable) { - LOG.warn("Failed to resolve Agent Workbench project runtime config for $projectRoot", t) - true - } - } -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/cost/AgentSessionUsageSnapshot.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/cost/AgentSessionUsageSnapshot.kt deleted file mode 100644 index aa2c5b538504..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/cost/AgentSessionUsageSnapshot.kt +++ /dev/null @@ -1,63 +0,0 @@ -// 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.cost - -import com.intellij.platform.ai.agent.core.session.AgentSessionCost -import com.intellij.platform.ai.agent.core.session.AgentSessionCostKind -import com.intellij.openapi.extensions.ExtensionPointName -import java.math.BigDecimal - -data class AgentSessionUsageSnapshot( - @JvmField val modelId: String?, - @JvmField val inputTokens: Long = 0, - @JvmField val outputTokens: Long = 0, - @JvmField val cacheReadTokens: Long = 0, - @JvmField val cacheWriteTokens: Long = 0, - @JvmField val cacheWrite5mTokens: Long = 0, - @JvmField val cacheWrite1hTokens: Long = 0, - @JvmField val reasoningTokens: Long = 0, - @JvmField val requestCount: Long = 0, - @JvmField val nativeExactCostUsd: BigDecimal? = null, -) - -fun List.aggregateAgentSessionUsageCost( - calculateCost: (AgentSessionUsageSnapshot) -> AgentSessionCost, -): AgentSessionCost? { - if (isEmpty()) return null - - val componentCosts = map(calculateCost) - if (componentCosts.any { it.amountUsd == null }) { - return AgentSessionCost(amountUsd = null, kind = AgentSessionCostKind.UNAVAILABLE) - } - - val totalAmount = componentCosts.fold(BigDecimal.ZERO) { acc, cost -> - acc + checkNotNull(cost.amountUsd) - } - val kind = if (componentCosts.all { it.kind == AgentSessionCostKind.EXACT }) { - AgentSessionCostKind.EXACT - } - else { - AgentSessionCostKind.ESTIMATED - } - val matchedModelId = componentCosts.mapNotNull(AgentSessionCost::matchedModelId).distinct().singleOrNull() - return AgentSessionCost(amountUsd = totalAmount, kind = kind, matchedModelId = matchedModelId) -} - -interface AgentSessionUsageCostCalculator { - fun calculateCost(usage: AgentSessionUsageSnapshot): AgentSessionCost -} - -object AgentSessionUsageCostCalculators { - val EP_NAME: ExtensionPointName = - ExtensionPointName("com.intellij.agent.workbench.sessionUsageCostCalculator") - - fun calculateCost(usage: AgentSessionUsageSnapshot): AgentSessionCost { - val calculator = EP_NAME.extensionList.firstOrNull() - if (calculator != null) { - return calculator.calculateCost(usage) - } - usage.nativeExactCostUsd?.let { exactCost -> - return AgentSessionCost(amountUsd = exactCost, kind = AgentSessionCostKind.EXACT) - } - return AgentSessionCost(amountUsd = null, kind = AgentSessionCostKind.UNAVAILABLE) - } -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionLaunchContributor.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionLaunchContributor.kt deleted file mode 100644 index f71165064104..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionLaunchContributor.kt +++ /dev/null @@ -1,95 +0,0 @@ -// 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.launch - -import com.intellij.platform.ai.agent.core.session.AgentSessionProvider -import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.extensions.ExtensionPointName -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.ensureActive - -/** - * Multi-extension hook for adding env variables / CLI args to a session's launch spec. - * Fires for **both** new sessions (via `createNewSession`) and resumed ones (via - * `resolveResume`), regardless of which open path the user took (threadView tree click, - * rename rebind, prompt-launch resume, etc.). All registered contributors are applied - * in order; later ones see the result of earlier ones. - * - * Distinct from [AgentSessionLaunchSpecAugmenter]: - * - That EP is single-extension and intended for *project*-level launch config - * (working dir, shell wrapper). - * - This EP is multi-extension and intended for the (project, provider) — and for - * resume launches additionally (sessionId) — to plug per-session decorations. - * Examples: [AwbMcpConfigContributor] regenerates the merged `--mcp-config` file - * with the current IDE URL on every launch; the container plugin's contributor - * rebinds resumed threads to active container sessions. - * - * `sessionId` is `null` for new launches (no thread exists yet) and the runtime thread - * id for resumes. Contributors that only make sense on resume (e.g. binding to an - * existing container session) should early-return when it is null. - * - * Contributors should be cheap and side-effect-free: they're called every time a - * session is launched, including UI-driven reopens. - */ -fun interface AgentSessionLaunchContributor { - /** - * Returns a (possibly augmented) launch spec. Return [launchSpec] unchanged when the - * contributor doesn't apply to this launch. - */ - suspend fun contribute( - projectPath: String, - projectDirectory: String?, - provider: AgentSessionProvider, - sessionId: String?, - launchSpec: AgentSessionTerminalLaunchSpec, - ): AgentSessionTerminalLaunchSpec -} - -private class AgentSessionLaunchContributorRegistry - -private val LOG = logger() - -private val AGENT_SESSION_LAUNCH_CONTRIBUTOR_EP: ExtensionPointName = - ExtensionPointName("com.intellij.agent.workbench.sessionLaunchContributor") - -object AgentSessionLaunchContributors { - suspend fun applyAll( - projectPath: String, - projectDirectory: String? = null, - provider: AgentSessionProvider, - sessionId: String?, - launchSpec: AgentSessionTerminalLaunchSpec, - ): AgentSessionTerminalLaunchSpec { - val contributors = AGENT_SESSION_LAUNCH_CONTRIBUTOR_EP.extensionList - if (contributors.isEmpty()) return launchSpec - var result = launchSpec - for (contributor in contributors) { - result = try { - contributor.contribute( - projectPath = projectPath, - projectDirectory = projectDirectory, - provider = provider, - sessionId = sessionId, - launchSpec = result, - ) - } - catch (e: CancellationException) { - // Coroutine cancellation must propagate — never log-and-swallow. - throw e - } - catch (e: Exception) { - // Don't let a broken plugin contributor poison the launch: log and use the - // pre-contributor result. `Error` (OOM, StackOverflow, …) is intentionally - // *not* caught here — those are JVM-fatal and should crash up the stack. - // Logged at `error` (not `warn`) so a failing extension is visible by - // default in `idea.log`; previously the `warn` masked classloading failures - // like the EP plugin XML pointing at a missing class name. - LOG.error("Launch contributor ${contributor::class.java.name} failed; skipping", e) - currentCoroutineContext().ensureActive() - result - } - } - return result - } -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionLaunchPlanner.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionLaunchPlanner.kt deleted file mode 100644 index ebafa8d3d04f..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionLaunchPlanner.kt +++ /dev/null @@ -1,189 +0,0 @@ -// 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.launch - -import com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode -import com.intellij.platform.ai.agent.core.session.AgentSessionProvider -import com.intellij.agent.workbench.prompt.core.AgentPromptGenerationModel -import com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings -import com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessagePlan -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.AgentSessionTerminalLaunchSpec -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.project.Project -import com.intellij.util.concurrency.ThreadingAssertions -import kotlin.coroutines.cancellation.CancellationException - -private val LOG = logger() - -enum class AgentSessionLaunchOperation { - NEW, - RESUME, -} - -data class AgentSessionLaunchIntent( - @JvmField val projectPath: String, - @JvmField val projectDirectory: String? = null, - val provider: AgentSessionProvider, - @JvmField val operation: AgentSessionLaunchOperation, - @JvmField val sessionId: String? = null, - @JvmField val launchMode: AgentSessionLaunchMode = AgentSessionLaunchMode.STANDARD, - @JvmField val launchTargetId: String? = null, - val surfaceId: AgentSessionSurfaceId? = null, - @JvmField val generationSettings: AgentPromptGenerationSettings = AgentPromptGenerationSettings.AUTO, -) - -data class AgentSessionPlannedLaunch( - @JvmField val intent: AgentSessionLaunchIntent, - @JvmField val baseLaunchSpec: AgentSessionTerminalLaunchSpec, - @JvmField val launchSpec: AgentSessionTerminalLaunchSpec, - @JvmField val generationModelCatalog: List, -) - -object AgentSessionLaunchPlanner { - suspend fun plan( - intent: AgentSessionLaunchIntent, - project: Project? = null, - initialMessagePlan: AgentInitialMessagePlan = AgentInitialMessagePlan.EMPTY, - generationModelCatalog: List = emptyList(), - extraEnvVariables: Map = emptyMap(), - extraCommandArgs: List = emptyList(), - resumeLaunchSpecProvider: (suspend (AgentSessionProvider, String, AgentSessionLaunchMode) -> AgentSessionTerminalLaunchSpec)? = null, - ): AgentSessionPlannedLaunch { - ThreadingAssertions.assertBackgroundThread() - return planImpl( - intent = intent, - project = project, - initialMessagePlan = initialMessagePlan, - generationModelCatalog = generationModelCatalog, - extraEnvVariables = extraEnvVariables, - extraCommandArgs = extraCommandArgs, - resumeLaunchSpecProvider = resumeLaunchSpecProvider, - ) - } - - private suspend fun planImpl( - intent: AgentSessionLaunchIntent, - project: Project?, - initialMessagePlan: AgentInitialMessagePlan, - generationModelCatalog: List, - extraEnvVariables: Map, - extraCommandArgs: List, - resumeLaunchSpecProvider: (suspend (AgentSessionProvider, String, AgentSessionLaunchMode) -> AgentSessionTerminalLaunchSpec)?, - ): AgentSessionPlannedLaunch { - val descriptor = AgentSessionProviders.find(intent.provider) - val baseLaunchSpec = buildBaseLaunchSpec(intent, descriptor, resumeLaunchSpecProvider) - val baseLaunchSpecWithWorkingDirectory = if (intent.projectDirectory != null && baseLaunchSpec.workingDirectory == null) { - baseLaunchSpec.copy(workingDirectory = intent.projectDirectory) - } - else { - baseLaunchSpec - } - val sanitizedIntent = intent.copy( - surfaceId = descriptor?.let { providerDescriptor -> effectiveAgentSessionSurfaceId(providerDescriptor, intent.surfaceId) } - ?: intent.surfaceId, - generationSettings = descriptor?.sanitizeGenerationSettings(intent.generationSettings) ?: AgentPromptGenerationSettings.AUTO, - ) - val generationLaunchSpec = descriptor?.applyGenerationSettings( - baseLaunchSpec = baseLaunchSpecWithWorkingDirectory, - generationSettings = sanitizedIntent.generationSettings, - initialMessagePlan = initialMessagePlan, - ) ?: baseLaunchSpecWithWorkingDirectory - val resolvedCatalog = resolveGenerationModelCatalog( - descriptor = descriptor, - project = project, - generationSettings = sanitizedIntent.generationSettings, - generationModelCatalog = generationModelCatalog, - ) - val catalogLaunchSpec = descriptor?.applyGenerationModelCatalog( - baseLaunchSpec = generationLaunchSpec, - generationSettings = sanitizedIntent.generationSettings, - generationModelCatalog = resolvedCatalog, - ) ?: generationLaunchSpec - val augmentedLaunchSpec = AgentSessionLaunchSpecs.augment( - projectPath = sanitizedIntent.projectPath, - projectDirectory = sanitizedIntent.projectDirectory, - provider = sanitizedIntent.provider, - launchSpec = catalogLaunchSpec, - ) - val contributedLaunchSpec = AgentSessionLaunchContributors.applyAll( - projectPath = sanitizedIntent.projectPath, - projectDirectory = sanitizedIntent.projectDirectory, - provider = sanitizedIntent.provider, - sessionId = sanitizedIntent.sessionId.takeIf { sanitizedIntent.operation == AgentSessionLaunchOperation.RESUME }, - launchSpec = augmentedLaunchSpec, - ) - return AgentSessionPlannedLaunch( - intent = sanitizedIntent, - baseLaunchSpec = baseLaunchSpec, - launchSpec = applyTransientExtras( - launchSpec = contributedLaunchSpec, - extraEnvVariables = extraEnvVariables, - extraCommandArgs = extraCommandArgs, - ), - generationModelCatalog = resolvedCatalog, - ) - } - - private suspend fun buildBaseLaunchSpec( - intent: AgentSessionLaunchIntent, - descriptor: AgentSessionProviderDescriptor?, - resumeLaunchSpecProvider: (suspend (AgentSessionProvider, String, AgentSessionLaunchMode) -> AgentSessionTerminalLaunchSpec)?, - ): AgentSessionTerminalLaunchSpec { - return when (intent.operation) { - AgentSessionLaunchOperation.NEW -> { - requireNotNull(descriptor) { "Missing Agent Workbench provider descriptor for ${intent.provider.value}" } - descriptor.buildNewSessionLaunchSpec(intent.launchMode) - } - AgentSessionLaunchOperation.RESUME -> { - val sessionId = requireNotNull(intent.sessionId) { "Resume launch intent must contain a session id" } - resumeLaunchSpecProvider?.let { provider -> - return provider(intent.provider, sessionId, intent.launchMode) - } - descriptor?.buildResumeLaunchSpec(sessionId, intent.launchMode) - ?: AgentSessionTerminalLaunchSpec(command = listOf(intent.provider.value, "resume", sessionId)) - } - } - } - - private suspend fun resolveGenerationModelCatalog( - descriptor: AgentSessionProviderDescriptor?, - project: Project?, - generationSettings: AgentPromptGenerationSettings, - generationModelCatalog: List, - ): List { - if (descriptor?.supportsGenerationModelSelection != true) { - return emptyList() - } - if (generationModelCatalog.isNotEmpty()) { - return generationModelCatalog - } - if (generationSettings == AgentPromptGenerationSettings.AUTO && !descriptor.resolvesGenerationModelCatalogForAutoSettings) { - return emptyList() - } - return try { - descriptor.listAvailableGenerationModels(project) - } - catch (e: CancellationException) { - throw e - } - catch (e: Throwable) { - LOG.warn("Failed to refresh generation model catalog for provider '${descriptor.provider.value}' during launch planning", e) - emptyList() - } - } -} - -private fun applyTransientExtras( - launchSpec: AgentSessionTerminalLaunchSpec, - extraEnvVariables: Map, - extraCommandArgs: List, -): AgentSessionTerminalLaunchSpec { - if (extraEnvVariables.isEmpty() && extraCommandArgs.isEmpty()) { - return launchSpec - } - return launchSpec.copy( - command = if (extraCommandArgs.isEmpty()) launchSpec.command else launchSpec.command + extraCommandArgs, - envVariables = if (extraEnvVariables.isEmpty()) launchSpec.envVariables else launchSpec.envVariables + extraEnvVariables, - ) -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionLaunchSpecAugmenter.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionLaunchSpecAugmenter.kt deleted file mode 100644 index f154743c44db..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionLaunchSpecAugmenter.kt +++ /dev/null @@ -1,56 +0,0 @@ -// 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.launch - -import com.intellij.platform.ai.agent.core.extensions.SingleExtensionPointResolver -import com.intellij.platform.ai.agent.core.extensions.SuspendingOverridableValue -import com.intellij.platform.ai.agent.core.session.AgentSessionProvider -import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.extensions.ExtensionPointName - -fun interface AgentSessionLaunchSpecAugmenter { - suspend fun augment( - projectPath: String, - projectDirectory: String?, - provider: AgentSessionProvider, - launchSpec: AgentSessionTerminalLaunchSpec, - ): AgentSessionTerminalLaunchSpec -} - -private class AgentSessionLaunchSpecAugmenterRegistry - -private val LOG = logger() -private val AGENT_SESSION_LAUNCH_SPEC_AUGMENTER_EP: ExtensionPointName = - ExtensionPointName("com.intellij.agent.workbench.sessionLaunchSpecAugmenter") - -private val REGISTERED_AUGMENTER = SingleExtensionPointResolver( - log = LOG, - extensionPoint = AGENT_SESSION_LAUNCH_SPEC_AUGMENTER_EP, - unavailableMessage = "Session launch spec augmenter EP is unavailable in this context", - multipleExtensionsMessage = { augmenters -> - "Multiple session launch spec augmenters registered; using first: ${augmenters.map { it::class.java.name }}" - }, -) - -object AgentSessionLaunchSpecAugmenters { - private val augmenterOverride = SuspendingOverridableValue { REGISTERED_AUGMENTER.findFirstOrNull() } - - suspend fun augment( - projectPath: String, - projectDirectory: String? = null, - provider: AgentSessionProvider, - launchSpec: AgentSessionTerminalLaunchSpec, - ): AgentSessionTerminalLaunchSpec { - val augmenter = augmenterOverride.value() ?: return launchSpec - return augmenter.augment( - projectPath = projectPath, - projectDirectory = projectDirectory, - provider = provider, - launchSpec = launchSpec, - ) - } - - suspend fun withAugmenterForTest(augmenter: AgentSessionLaunchSpecAugmenter?, action: suspend () -> T): T { - return augmenterOverride.withOverride(augmenter, action) - } -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionLaunchSpecs.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionLaunchSpecs.kt deleted file mode 100644 index 11070c6e0d30..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionLaunchSpecs.kt +++ /dev/null @@ -1,93 +0,0 @@ -// 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.launch - -import com.intellij.platform.ai.agent.core.session.AgentSessionProvider -import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec - -object AgentSessionLaunchSpecs { - suspend fun augment( - projectPath: String, - projectDirectory: String? = null, - provider: AgentSessionProvider, - launchSpec: AgentSessionTerminalLaunchSpec, - ): AgentSessionTerminalLaunchSpec { - return AgentSessionLaunchSpecAugmenters.augment( - projectPath = projectPath, - projectDirectory = projectDirectory, - provider = provider, - launchSpec = launchSpec, - ) - } - -} - -fun replaceOrAddOption( - command: List, - option: String, - value: String, - beforeTokens: Set = setOf("--"), -): List { - val boundaryIndex = findBoundaryIndex(command, beforeTokens) - for (index in 0.., - options: Set, - beforeTokens: Set = emptySet(), -): List { - val boundaryIndex = findBoundaryIndex(command, beforeTokens) - val result = mutableListOf() - var changed = false - var index = 0 - while (index < boundaryIndex) { - val token = command[index] - if (token in options) { - changed = true - index += if (index + 1 < boundaryIndex) 2 else 1 - } - else { - result.add(token) - index++ - } - } - - if (!changed) return command - - result.addAll(command.subList(boundaryIndex, command.size)) - return result -} - -fun insertArgumentsBefore( - command: List, - arguments: List, - beforeTokens: Set, -): List { - if (arguments.isEmpty()) return command - - val boundaryIndex = findBoundaryIndex(command, beforeTokens) - return command.toMutableList().apply { - addAll(boundaryIndex, arguments) - } -} - -private fun findBoundaryIndex(command: List, beforeTokens: Set): Int { - if (beforeTokens.isEmpty()) return command.size - return command.indexOfFirst { token -> token in beforeTokens }.takeIf { index -> index >= 0 } ?: command.size -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionOutOfBandLaunch.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionOutOfBandLaunch.kt deleted file mode 100644 index 165875d55ac9..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionOutOfBandLaunch.kt +++ /dev/null @@ -1,49 +0,0 @@ -// 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.launch - -import com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings -import com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode -import com.intellij.platform.ai.agent.core.session.AgentSessionProvider -import com.intellij.openapi.extensions.ExtensionPointName -import com.intellij.openapi.project.Project - -data class AgentSessionOutOfBandLaunchContext( - val provider: AgentSessionProvider, - @JvmField val launchMode: AgentSessionLaunchMode, - @JvmField val launchProfileId: String?, - @JvmField val launchTargetId: String?, - val surfaceId: AgentSessionSurfaceId, - @JvmField val generationSettings: AgentPromptGenerationSettings, -) - -/** - * Delivers a new session's launch out of band — without a managed terminal. Used by non-terminal - * runtimes (e.g. ACP): the threadView tab renders custom content, and the chosen agent + initial prompt are - * applied by the implementation instead of being dispatched into a terminal. - * - * Looked up by resolved launch context; when one handles the context, the launch service skips - * terminal dispatch and invokes [launch] once the threadView tab has opened (so a real [Project] is available). - */ -interface AgentSessionOutOfBandLaunch { - fun handles(provider: AgentSessionProvider): Boolean - - fun handles(context: AgentSessionOutOfBandLaunchContext): Boolean = handles(context.provider) - - /** - * @param threadId the concrete session id the threadView tab opened with (the provider's preallocated id) - * @param context the resolved launch context, including the optional launch target selected by the profile - * @param prompt the initial prompt text, or `null`/blank when none - */ - suspend fun launch(project: Project, path: String, threadId: String, context: AgentSessionOutOfBandLaunchContext, prompt: String?) - - companion object { - private val EP = ExtensionPointName("com.intellij.agent.workbench.sessionOutOfBandLaunch") - - @Suppress("unused") - fun forProvider(provider: AgentSessionProvider): AgentSessionOutOfBandLaunch? = - if (EP.hasAnyExtensions()) EP.extensionList.firstOrNull { it.handles(provider) } else null - - fun forContext(context: AgentSessionOutOfBandLaunchContext): AgentSessionOutOfBandLaunch? = - if (EP.hasAnyExtensions()) EP.extensionList.firstOrNull { it.handles(context) } else null - } -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionSurfaces.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionSurfaces.kt deleted file mode 100644 index 19a764ce6957..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionSurfaces.kt +++ /dev/null @@ -1,97 +0,0 @@ -// 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.launch - -import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProviderDescriptor -import com.intellij.openapi.extensions.ExtensionPointName -import org.jetbrains.annotations.ApiStatus -import java.util.Locale - -private val AGENT_SESSION_SURFACE_ID_REGEX = Regex("[a-z][a-z0-9._-]*") - -@ApiStatus.Internal -@JvmInline -value class AgentSessionSurfaceId private constructor(val value: String) { - companion object { - fun from(value: String): AgentSessionSurfaceId { - val normalized = normalizeAgentSessionSurfaceValue(value) - require(normalized != null && AGENT_SESSION_SURFACE_ID_REGEX.matches(normalized)) { - "Invalid surface id '$value'. Expected: ${AGENT_SESSION_SURFACE_ID_REGEX.pattern}" - } - return AgentSessionSurfaceId(normalized) - } - - fun fromOrNull(value: String?): AgentSessionSurfaceId? { - val normalized = normalizeAgentSessionSurfaceValue(value) ?: return null - return if (AGENT_SESSION_SURFACE_ID_REGEX.matches(normalized)) AgentSessionSurfaceId(normalized) else null - } - } - - override fun toString(): String = value -} - -@ApiStatus.Internal -const val AGENT_SESSION_SURFACE_TERMINAL: String = "terminal" - -@ApiStatus.Internal -const val AGENT_SESSION_SURFACE_ACP: String = "acp" - -@ApiStatus.Internal -data class AgentSessionSurfaceDescriptor( - val id: AgentSessionSurfaceId, -) - -@ApiStatus.Internal -interface AgentSessionSurfaceContributor { - fun getSurfaces(): List -} - -@ApiStatus.Internal -object AgentSessionSurfaces { - val TERMINAL: AgentSessionSurfaceId = AgentSessionSurfaceId.from(AGENT_SESSION_SURFACE_TERMINAL) - val ACP: AgentSessionSurfaceId = AgentSessionSurfaceId.from(AGENT_SESSION_SURFACE_ACP) - - private val EP = ExtensionPointName("com.intellij.agent.workbench.sessionSurfaceContributor") - private val BUILT_IN_DESCRIPTORS = listOf( - AgentSessionSurfaceDescriptor(TERMINAL), - AgentSessionSurfaceDescriptor(ACP), - ) - - fun all(): List { - val contributed = if (EP.hasAnyExtensions()) EP.extensionList.flatMap { contributor -> contributor.getSurfaces() } else emptyList() - return (BUILT_IN_DESCRIPTORS + contributed).distinctBy { descriptor -> descriptor.id } - } - - fun find(id: AgentSessionSurfaceId): AgentSessionSurfaceDescriptor? { - return all().firstOrNull { descriptor -> descriptor.id == id } - } -} - -@ApiStatus.Internal -fun normalizeAgentSessionSurfaceId(surfaceId: String?): String? { - return AgentSessionSurfaceId.fromOrNull(surfaceId)?.value -} - -@ApiStatus.Internal -fun effectiveAgentSessionSurfaceId( - descriptor: AgentSessionProviderDescriptor, - surfaceId: String?, -): AgentSessionSurfaceId { - return effectiveAgentSessionSurfaceId(descriptor, AgentSessionSurfaceId.fromOrNull(surfaceId)) -} - -@ApiStatus.Internal -fun effectiveAgentSessionSurfaceId( - descriptor: AgentSessionProviderDescriptor, - surfaceId: AgentSessionSurfaceId?, -): AgentSessionSurfaceId { - val supportedSurfaces = descriptor.supportedLaunchSurfaces - if (surfaceId != null && surfaceId in supportedSurfaces) { - return surfaceId - } - return descriptor.defaultLaunchSurface.takeIf { defaultSurface -> defaultSurface in supportedSurfaces } - ?: AgentSessionSurfaces.TERMINAL -} - -private fun normalizeAgentSessionSurfaceValue(surfaceId: String?): String? { - return surfaceId?.trim()?.lowercase(Locale.ROOT)?.takeIf(String::isNotEmpty) -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionThreadViewOpenPlan.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionThreadViewOpenPlan.kt deleted file mode 100644 index 6fbd2b2a3594..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AgentSessionThreadViewOpenPlan.kt +++ /dev/null @@ -1,67 +0,0 @@ -// 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.launch - -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 -import com.intellij.platform.ai.agent.core.session.AgentSessionThread -import com.intellij.platform.ai.agent.core.session.AgentSubAgent -import com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings -import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec -import com.intellij.openapi.project.Project -import com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialPromptDeliveryPlan -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -data class AgentSessionThreadViewOpenPlan( - @JvmField val threadIdentity: String, - @JvmField val launchSpec: AgentSessionTerminalLaunchSpec, - @JvmField val runtimeThreadId: String, - @JvmField val threadTitle: String, - @JvmField val subAgentId: String?, - @JvmField val initialMessageDispatchPlan: AgentInitialPromptDeliveryPlan = AgentInitialPromptDeliveryPlan.EMPTY, -) - -suspend fun resolveAgentSessionThreadViewOpenPlan( - projectPath: String, - projectDirectory: String? = null, - thread: AgentSessionThread, - subAgent: AgentSubAgent?, - launchSpecOverride: AgentSessionTerminalLaunchSpec?, - launchMode: AgentSessionLaunchMode = AgentSessionLaunchMode.STANDARD, - launchTargetId: String? = null, - surfaceId: AgentSessionSurfaceId? = null, - generationSettings: AgentPromptGenerationSettings = AgentPromptGenerationSettings.AUTO, - project: Project? = null, - resumeLaunchSpecProvider: (suspend (AgentSessionProvider, String, AgentSessionLaunchMode) -> AgentSessionTerminalLaunchSpec)? = null, -): AgentSessionThreadViewOpenPlan { - val threadIdentity = buildAgentThreadIdentity(providerId = thread.provider.value, threadId = thread.id) - val runtimeThreadId = subAgent?.id ?: thread.id - val launchSpec = launchSpecOverride - ?: withContext(Dispatchers.Default) { - AgentSessionLaunchPlanner.plan( - intent = AgentSessionLaunchIntent( - projectPath = projectPath, - projectDirectory = projectDirectory, - provider = thread.provider, - operation = AgentSessionLaunchOperation.RESUME, - sessionId = runtimeThreadId, - launchMode = launchMode, - launchTargetId = launchTargetId, - surfaceId = surfaceId, - generationSettings = generationSettings, - ), - project = project, - resumeLaunchSpecProvider = resumeLaunchSpecProvider, - ).launchSpec - } - val threadTitle = subAgent?.name?.ifBlank { subAgent.id } ?: thread.title - return AgentSessionThreadViewOpenPlan( - threadIdentity = threadIdentity, - launchSpec = launchSpec, - runtimeThreadId = runtimeThreadId, - threadTitle = threadTitle, - subAgentId = subAgent?.id, - initialMessageDispatchPlan = AgentInitialPromptDeliveryPlan.EMPTY, - ) -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AwbMcpConfigBuilder.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AwbMcpConfigBuilder.kt deleted file mode 100644 index de393442f020..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AwbMcpConfigBuilder.kt +++ /dev/null @@ -1,260 +0,0 @@ -// 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.launch - -import com.intellij.platform.ai.agent.core.session.AgentSessionProvider -import com.intellij.platform.ai.agent.json.createJsonGenerator -import com.intellij.platform.ai.agent.json.createJsonParser -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.util.registry.Registry -import tools.jackson.core.JsonParser -import tools.jackson.core.JsonToken -import tools.jackson.core.json.JsonFactory -import tools.jackson.core.util.DefaultPrettyPrinter -import java.io.StringWriter -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.StandardOpenOption - -/** - * Builds and writes the AWB-managed merged MCP config file used by the agent CLI. - * - * Why a generated file: relying on Claude Code's `${VAR}` expansion in the auto-loaded - * project-root `.mcp.json` proved unreliable in practice — the literal placeholder leaked - * into the URL field and Claude failed with `"/" cannot be parsed as a URL against - * "${JETBRAINS_IDE_MCP_URL}"`. With `--mcp-config --strict-mcp-config`, Claude - * reads only our file, with already-resolved literal values. No env-var expansion needed. - * - * The file lives at `/.awb/awb-mcp.json` and is **regenerated on every - * AWB-driven launch** (new + resume), so each spawned CLI sees the IDE's *current* - * streamable-HTTP MCP URL even after IDE restarts that reassign the port. - * - * The merge keeps the user's other MCP servers (Glean, etc.) usable: we copy - * entries from configured MCP files verbatim into our file, dropping any name registered - * by an [AwbMcpConfigProviderContributor]. The Jackson streaming parser preserves - * unknown fields exactly. - * - * **Per-provider details live in [AwbMcpConfigProviderContributor] implementations** - * (registered by claude/codex/junie modules), not here. This class only orchestrates: - * registry gate → contributor lookup → file write → CLI args. Adding a new provider - * doesn't require any changes to sessions-core. - * - * When the registry key `agent.workbench.mcp.use.direct.http` is OFF, no contributor - * handles the active provider, or the local IDE doesn't expose an `mcpServerUrl`, - * [buildForLaunch] returns `null` and callers fall back to whatever the user's - * `.mcp.json` already does (e.g. ij-proxy stdio). This keeps the legacy path uninvolved. - */ -object AwbMcpConfigBuilder { - private val LOG = logger() - private const val REGISTRY_KEY = "agent.workbench.mcp.use.direct.http" - private const val USER_MCP_FILE = ".mcp.json" - const val AWB_DIR: String = ".awb" - private const val GENERATED_FILE = "awb-mcp.json" - - /** - * Names that AWB-agnostic legacy proxies use in users' `.mcp.json`. Always filtered - * out during merge — these aren't tied to any specific provider, so they don't - * belong in a contributor. - */ - val LEGACY_FILTERED_NAMES: Set = setOf("ijproxy", "ij-proxy") - - /** - * Pins server-side project resolution to a specific project path. Read by - * `McpProjectLocationInputs.resolveProject` to disambiguate when several IDE projects - * are open against the same MCP server. - */ - const val PROJECT_PATH_ENV: String = "JETBRAINS_MCP_PROJECT_PATH" - - data class LaunchConfig( - /** CLI args to append to the agent command, e.g. `--mcp-config --strict-mcp-config`. */ - val extraArgs: List, - /** Env variables for project disambiguation. */ - val envVariables: Map, - /** Generated config file path; written at this exact location. */ - val configFile: Path, - ) - - /** - * Returns true when AWB should prefer the local IDE streamable-HTTP MCP URL - * over provider-specific legacy MCP discovery/configuration. - */ - fun isDirectHttpEnabled(): Boolean = Registry.`is`(REGISTRY_KEY, false) - - /** - * Returns true when AWB should manage the agent's MCP config via the merged - * `--mcp-config ` route for [provider]. Two gates: - * - The registry key `agent.workbench.mcp.use.direct.http` must be on. - * - Some [AwbMcpConfigProviderContributor] must accept [provider] (i.e. the provider's - * CLI is known to support a compatible `--mcp-config ` invocation). - */ - fun isEnabled(provider: AgentSessionProvider): Boolean = - isDirectHttpEnabled() && findContribution(provider) != null - - /** - * Returns a [LaunchConfig] when [isEnabled] holds for [provider] AND the local IDE - * reports an `mcpServerUrl`. Returns `null` otherwise — the caller falls back to its - * existing launch path (typically the user's checked-in `.mcp.json`). - * - * Uses [McpStreamUrlProvider.resolve] (the in-process `McpServerService` URL) rather - * than parsing the per-PID discovery JSONs: AWB always launches from inside the IDE - * that owns the project, so the local URL is by definition the correct one — and - * reading our own discovery file would be wrong if it's stale or hasn't been written - * yet during early startup. - * - * Side effect: writes `/.awb/awb-mcp.json` with the merged config and a - * literal IDE MCP URL. - */ - fun buildForLaunch(projectPath: Path, provider: AgentSessionProvider): LaunchConfig? { - if (!isDirectHttpEnabled()) return null - val contribution = findContribution(provider) ?: return null - val mcpUrl = McpStreamUrlProvider.resolve() ?: run { - LOG.info("No MCP stream URL available for $projectPath; falling back to user's .mcp.json") - return null - } - val configFile = writeMergedConfigFile(projectPath, mcpUrl, contribution.mcpServerNames) - return LaunchConfig( - extraArgs = contribution.buildCliArgs(configFile), - envVariables = mapOf(PROJECT_PATH_ENV to projectPath.toString()), - configFile = configFile, - ) - } - - /** - * Deterministic per-project config file path under `.awb/`. - */ - fun configFilePath(projectPath: Path): Path = - projectPath.resolve(AWB_DIR).resolve(GENERATED_FILE) - - /** - * Writes the merged config to disk. Public so callers can pre-generate before - * constructing a launch spec without going through [buildForLaunch]. - */ - fun writeMergedConfigFile( - projectPath: Path, - mcpUrl: String, - ourServerNames: Set, - additionalMcpConfigFiles: List = emptyList(), - ): Path { - val path = configFilePath(projectPath) - Files.createDirectories(path.parent) - Files.writeString( - path, - buildMergedConfigJson(projectPath, mcpUrl, ourServerNames, additionalMcpConfigFiles), - StandardOpenOption.CREATE, - StandardOpenOption.TRUNCATE_EXISTING, - ) - return path - } - - private fun findContribution(provider: AgentSessionProvider): AwbMcpConfig? { - for (contributor in AwbMcpConfigProviderContributor.EP.extensionList) { - val contribution = contributor.contribute(provider) ?: continue - return contribution - } - return null - } - - /** Union of legacy proxy names and every contributor's declared bridge names. */ - private fun filteredNames(): Set { - val result = LEGACY_FILTERED_NAMES.toMutableSet() - for (contributor in AwbMcpConfigProviderContributor.EP.extensionList) { - result += contributor.filteredServerNames - } - return result - } - - private fun buildMergedConfigJson( - projectPath: Path, - mcpUrl: String, - ourServerNames: Set, - additionalMcpConfigFiles: List, - ): String { - val filtered = filteredNames() + ourServerNames - val factory = JsonFactory() - val userServers = collectUserMcpServers( - factory = factory, - mcpConfigFiles = (additionalMcpConfigFiles + projectPath.resolve(USER_MCP_FILE)).distinct(), - filtered = filtered, - ) - val out = StringWriter() - factory.createJsonGenerator(out, DefaultPrettyPrinter()).use { gen -> - gen.writeStartObject() - gen.writeName("mcpServers") - gen.writeStartObject() - - for ((name, value) in userServers) { - gen.writeName(name) - gen.writeRawValue(value) - } - - // Our entries are written last. All names point at the same IDE MCP URL; some - // agent tooling expects specific bridge names, so multiple aliases keep every - // variant resolvable. - for (name in ourServerNames) { - gen.writeName(name) - gen.writeStartObject() - gen.writeStringProperty("type", "http") - gen.writeStringProperty("url", mcpUrl) - gen.writeEndObject() - } - - gen.writeEndObject() // mcpServers - gen.writeEndObject() // root - } - return out.toString() - } - - private fun collectUserMcpServers( - factory: JsonFactory, - mcpConfigFiles: List, - filtered: Set, - ): Map { - val result = LinkedHashMap() - for (userMcp in mcpConfigFiles) { - if (!Files.isRegularFile(userMcp)) continue - try { - collectUserMcpServers(factory, userMcp, filtered, result) - } - catch (e: Exception) { - LOG.warn("Failed to merge $userMcp; continuing with other MCP entries", e) - } - } - return result - } - - private fun collectUserMcpServers( - factory: JsonFactory, - userMcp: Path, - filtered: Set, - target: LinkedHashMap, - ) { - factory.createJsonParser(Files.newBufferedReader(userMcp)).use { parser -> - // Find the top-level "mcpServers" field. - while (parser.nextToken() != null) { - val name = parser.currentName() - if (parser.currentToken() == JsonToken.PROPERTY_NAME && name == "mcpServers") { - if (parser.nextToken() != JsonToken.START_OBJECT) return - while (parser.nextToken() != JsonToken.END_OBJECT) { - if (parser.currentToken() != JsonToken.PROPERTY_NAME) continue - val serverName = parser.currentName() - parser.nextToken() // advance to value - if (serverName in filtered) { - parser.skipChildren() - continue - } - target.remove(serverName) - target[serverName] = copyJsonValue(factory, parser) - } - return - } - } - } - } - - private fun copyJsonValue(factory: JsonFactory, parser: JsonParser): String { - val out = StringWriter() - factory.createJsonGenerator(out).use { gen -> - gen.copyCurrentStructure(parser) - } - return out.toString() - } -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AwbMcpConfigContributor.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AwbMcpConfigContributor.kt deleted file mode 100644 index 350bc40508ab..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AwbMcpConfigContributor.kt +++ /dev/null @@ -1,51 +0,0 @@ -// 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.launch - -import com.intellij.platform.ai.agent.core.normalizeAgentWorkbenchPath -import com.intellij.platform.ai.agent.core.session.AgentSessionProvider -import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec -import com.intellij.openapi.diagnostic.logger -import java.nio.file.Path - -/** - * Always-on launch contributor that regenerates the AWB-managed merged MCP config file - * (`/.awb/awb-mcp.json`) and points the spawned CLI at it via - * `--mcp-config`. Fires for **both** new sessions and resumed ones: a tab reopen after - * IDE restart picks up the current IDE MCP URL, and a fresh prompt-launch wires the - * file through too without going through the container launcher's direct call. - * - * Provider-agnostic: any AWB launch with a project that has an IDE serving an MCP URL - * benefits, container or not. The underlying [AwbMcpConfigBuilder] gates on the - * registry flag and the per-provider [AwbMcpConfigProviderContributor], so launches - * for providers without file-based config support are no-ops. - */ -internal class AwbMcpConfigContributor : AgentSessionLaunchContributor { - override suspend fun contribute( - projectPath: String, - projectDirectory: String?, - provider: AgentSessionProvider, - sessionId: String?, - launchSpec: AgentSessionTerminalLaunchSpec, - ): AgentSessionTerminalLaunchSpec { - val targetPath = projectDirectory ?: projectPath - val target = try { - Path.of(normalizeAgentWorkbenchPath(targetPath)) - } - catch (e: Exception) { - LOG.debug("Cannot normalize projectPath for AwbMcpConfig launch: $targetPath", e) - return launchSpec - } - val config = AwbMcpConfigBuilder.buildForLaunch(target, provider) ?: return launchSpec - LOG.info( - "Launch contributor wrote MCP config for ${provider.value}:${sessionId ?: ""} → ${config.configFile}" - ) - return launchSpec.copy( - command = launchSpec.command + config.extraArgs, - envVariables = launchSpec.envVariables + config.envVariables, - ) - } - - companion object { - private val LOG = logger() - } -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AwbMcpConfigProviderContributor.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AwbMcpConfigProviderContributor.kt deleted file mode 100644 index 62934746e96a..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/launch/AwbMcpConfigProviderContributor.kt +++ /dev/null @@ -1,77 +0,0 @@ -// 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.launch - -import com.intellij.platform.ai.agent.core.session.AgentSessionProvider -import com.intellij.openapi.extensions.ExtensionPointName -import java.nio.file.Path - -/** - * Per-provider plug-point for the AWB-managed MCP launch contract. - * - * Each provider module (claude, codex, junie, …) registers one of these from its own - * plugin XML to declare: - * - * - **Which `.mcp.json` server names belong to its IDE bridge.** The union across all - * contributors is dropped from the user's `.mcp.json` during merge so the AWB-managed - * file never carries stale bridge entries from a different provider's tooling. - * - **Whether AWB drives that provider via the file-based merged config**, by returning - * a non-null [contribute] for it. Only providers whose CLI accepts something - * compatible with `--mcp-config --strict-mcp-config` should opt in; others - * (Codex's `apply` mode, agents that auto-discover `.mcp.json`) should leave - * [contribute] returning null and let AWB fall through to the user's checked-in - * `.mcp.json`. - * - * Keeping this knowledge in the provider modules — rather than hard-coding it in - * [AwbMcpConfigBuilder] — means adding a new provider doesn't touch sessions-core at - * all. Distinct from [AwbMcpConfigContributor], which is the single launch-time hook - * that fires for both new and resumed sessions and orchestrates these per-provider - * contributions into the actual launch spec. - */ -interface AwbMcpConfigProviderContributor { - /** - * Server names this contributor's bridge claims in users' `.mcp.json`. Always - * filtered out during merge regardless of which provider is actively launching. - * A provider may declare multiple names (e.g. legacy spellings). - */ - val filteredServerNames: Set - - /** - * Returns the per-provider MCP config for [provider] when this contributor handles - * it, or null otherwise. The non-null result tells [AwbMcpConfigBuilder] both that - * it can build a merged config for this launch and how to wire the agent CLI to it. - * - * Default returns `null` for contributors that exist solely to register names in - * [filteredServerNames] (so the user's `.mcp.json` entries with those names are - * dropped during merge) without otherwise taking over the launch. - */ - fun contribute(provider: AgentSessionProvider): AwbMcpConfig? = null - - companion object { - val EP: ExtensionPointName = - ExtensionPointName("com.intellij.agent.workbench.mcpConfigProviderContributor") - } -} - -/** - * What a single provider contributes to a launch, once - * [AwbMcpConfigProviderContributor.contribute] has determined it handles the active - * provider. - */ -data class AwbMcpConfig( - /** - * Names under which the IDE-bridge entry is written in the merged config file. All - * entries point at the same IDE MCP URL — multiple names exist when an agent's - * tooling expects a specific bridge name (e.g. Claude Code refers to - * `ij`/`ij-container` interchangeably depending on the version). Each name - * registered here should also appear in - * [AwbMcpConfigProviderContributor.filteredServerNames] so a user-supplied entry of - * the same name is dropped during merge — otherwise our always-last write would - * still overlap with the user's entry's body. - */ - val mcpServerNames: Set, - /** - * CLI args to append to the agent command. Typical shape: - * `["--mcp-config", configFile, "--strict-mcp-config"]`. - */ - val buildCliArgs: (configFile: Path) -> List, -) diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/launch/McpStreamUrlProvider.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/launch/McpStreamUrlProvider.kt deleted file mode 100644 index 8cd42380ee66..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/launch/McpStreamUrlProvider.kt +++ /dev/null @@ -1,32 +0,0 @@ -// 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.launch - -import com.intellij.openapi.extensions.ExtensionPointName - -/** - * Returns the **local** IDE's streamable-HTTP MCP URL — i.e. the one that the agent CLI - * AWB is about to spawn should connect to. The implementation lives in a module that - * depends on the MCP Server plugin (`container-mcp`) and reads the URL straight from the - * in-process `McpServerConnectionAddressProvider`. - * - * "Local" is intentional: AWB always launches from inside the IDE that owns the project, - * so the in-process URL is by definition the correct one. There is no fallback to - * cross-process IDE discovery — picking a different IDE's URL would route the agent at - * an instance that doesn't hold the AWB session state for this launch. - */ -fun interface McpStreamUrlProvider { - fun getStreamUrl(): String? - - companion object { - private val EP: ExtensionPointName = - ExtensionPointName("com.intellij.agent.workbench.mcpStreamUrlProvider") - - fun resolve(): String? { - for (provider in EP.extensionList) { - val url = provider.getStreamUrl() - if (url != null) return url - } - return null - } - } -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/paths/AgentWorkbenchProjectPaths.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/paths/AgentWorkbenchProjectPaths.kt deleted file mode 100644 index 893d87998393..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/paths/AgentWorkbenchProjectPaths.kt +++ /dev/null @@ -1,79 +0,0 @@ -// 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.paths - -import com.intellij.platform.ai.agent.core.normalizeAgentWorkbenchPath -import com.intellij.platform.ai.agent.core.parseAgentWorkbenchPathOrNull -import org.jetbrains.annotations.ApiStatus -import java.nio.file.Files -import java.nio.file.Path -import kotlin.io.path.invariantSeparatorsPathString - -private val BAZEL_PROJECT_FILE_SUFFIXES = listOf( - ".bazelproject", - ".blazeproject", -) - -private val PROJECT_FILE_SUFFIXES = listOf( - ".ipr", - ".iws", - *BAZEL_PROJECT_FILE_SUFFIXES.toTypedArray(), -) - -@ApiStatus.Internal -fun resolveAgentWorkbenchProjectDirectory(identityPath: String?, projectBasePath: String? = null): String? { - val normalizedBasePath = projectBasePath - ?.takeIf { it.isNotBlank() } - ?.let(::normalizeAgentWorkbenchPath) - if (normalizedBasePath != null) { - return normalizedBasePath - } - - val parsedIdentityPath = identityPath - ?.takeIf { it.isNotBlank() } - ?.let(::normalizeAgentWorkbenchPath) - ?.let(::parseAgentWorkbenchPathOrNull) - ?: return null - return resolveAgentWorkbenchProjectDirectory(parsedIdentityPath).invariantSeparatorsPathString -} - -@ApiStatus.Internal -fun resolveAgentWorkbenchOwningProjectBasePath(identityPath: String?, projectBasePath: String?): String? { - val normalizedBasePath = projectBasePath - ?.takeIf { it.isNotBlank() } - ?.let(::normalizeAgentWorkbenchPath) - ?: return null - val normalizedIdentityPath = identityPath - ?.takeIf { it.isNotBlank() } - ?.let(::normalizeAgentWorkbenchPath) - ?: return null - val basePath = parseAgentWorkbenchPathOrNull(normalizedBasePath)?.normalize() ?: return null - val path = parseAgentWorkbenchPathOrNull(normalizedIdentityPath)?.normalize() ?: return null - return normalizedBasePath.takeIf { path == basePath || path.startsWith(basePath) } -} - -@ApiStatus.Internal -fun resolveAgentWorkbenchProjectDirectory(path: Path): Path { - val normalizedPath = path.normalize() - val fileName = normalizedPath.fileName?.toString().orEmpty() - val parentName = normalizedPath.parent?.fileName?.toString() - return when { - fileName == ".idea" -> normalizedPath.parent - parentName == ".idea" -> normalizedPath.parent?.parent - BAZEL_PROJECT_FILE_SUFFIXES.any { suffix -> fileName.endsWith(suffix, ignoreCase = true) } -> - normalizedPath.parent?.let(::resolveContainingGitWorktreeRoot) ?: normalizedPath.parent - PROJECT_FILE_SUFFIXES.any { suffix -> fileName.endsWith(suffix, ignoreCase = true) } -> normalizedPath.parent - else -> normalizedPath - } ?: normalizedPath -} - -private fun resolveContainingGitWorktreeRoot(startDirectory: Path): Path? { - var directory: Path? = startDirectory.normalize() - while (directory != null) { - val gitPath = directory.resolve(".git") - if (Files.isDirectory(gitPath) || Files.isRegularFile(gitPath)) { - return directory - } - directory = directory.parent - } - return null -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentOpenTopLevelThreadDispatchService.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentOpenTopLevelThreadDispatchService.kt deleted file mode 100644 index 01a5819ceb41..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentOpenTopLevelThreadDispatchService.kt +++ /dev/null @@ -1,14 +0,0 @@ -// 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.providers - -import com.intellij.platform.ai.agent.core.session.AgentSessionProvider - -interface AgentOpenTopLevelThreadDispatchService { - suspend fun dispatchIfPresent( - projectPath: String, - provider: AgentSessionProvider, - threadId: String, - launchSpec: AgentSessionTerminalLaunchSpec, - initialMessageDispatchPlan: AgentInitialPromptDeliveryPlan, - ): Boolean -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentPromptProviderOption.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentPromptProviderOption.kt deleted file mode 100644 index 00b475139372..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentPromptProviderOption.kt +++ /dev/null @@ -1,47 +0,0 @@ -// 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.providers - -import org.jetbrains.annotations.ApiStatus - -const val AGENT_PROMPT_PROVIDER_OPTION_PLAN_MODE: String = "plan_mode" - -val AGENT_PROMPT_PROVIDER_PLAN_MODE_OPTION: AgentPromptProviderOption = AgentPromptProviderOption( - id = AGENT_PROMPT_PROVIDER_OPTION_PLAN_MODE, - labelKey = "toolwindow.prompt.option.plan.mode", - labelFallback = "Plan mode", - defaultSelected = true, -) - -data class AgentPromptProviderOption( - @JvmField val id: String, - @JvmField val labelKey: String, - @JvmField val labelFallback: String, - @JvmField val defaultSelected: Boolean = false, - @JvmField val enabledForNewTask: Boolean = true, - @JvmField val enabledForExistingTask: Boolean = true, -) - -enum class AgentPromptProviderOptionTarget { - NEW_TASK, - EXISTING_TASK, -} - -@ApiStatus.Internal -fun resolveEffectiveProviderOptionIds( - selectedProvider: AgentSessionProviderDescriptor?, - selectedOptionIds: Set, - target: AgentPromptProviderOptionTarget, -): Set { - val bridge = selectedProvider ?: return emptySet() - return bridge.promptOptions - .asSequence() - .filter { option -> option.id in selectedOptionIds } - .filter { option -> - when (target) { - AgentPromptProviderOptionTarget.NEW_TASK -> option.enabledForNewTask - AgentPromptProviderOptionTarget.EXISTING_TASK -> option.enabledForExistingTask - } - } - .map(AgentPromptProviderOption::id) - .toSet() -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionLaunchProfiles.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionLaunchProfiles.kt deleted file mode 100644 index bd79ac5cc40a..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionLaunchProfiles.kt +++ /dev/null @@ -1,248 +0,0 @@ -// 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.providers - -import com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode -import com.intellij.platform.ai.agent.core.session.AgentSessionProvider -import com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings -import com.intellij.agent.workbench.prompt.core.AgentPromptInitialMessageRequest -import com.intellij.agent.workbench.prompt.core.AgentPromptLaunchProfile -import com.intellij.agent.workbench.prompt.core.AgentPromptLaunchProfileKind -import com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionSurfaceId -import com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionSurfaces -import com.intellij.platform.ai.agent.sessions.core.launch.effectiveAgentSessionSurfaceId -import com.intellij.platform.ai.agent.sessions.core.launch.normalizeAgentSessionSurfaceId -import com.intellij.openapi.extensions.ExtensionPointName -import com.intellij.openapi.project.Project -import org.jetbrains.annotations.ApiStatus -import java.nio.charset.StandardCharsets -import java.util.Base64 -import javax.swing.Icon - -const val BUILT_IN_LAUNCH_PROFILE_PREFIX: String = "builtin:" -private const val ACP_PROVIDER_ID = "acp" - -@ApiStatus.Internal -data class AgentSessionBuiltInLaunchProfileContribution( - @JvmField val id: String, - @JvmField val name: String, - val provider: AgentSessionProvider, - @JvmField val launchMode: AgentSessionLaunchMode = AgentSessionLaunchMode.STANDARD, - @JvmField val launchTargetId: String? = null, - val surfaceId: AgentSessionSurfaceId? = null, - @JvmField val icon: Icon? = null, -) - -@ApiStatus.Internal -interface AgentSessionLaunchProfileContributor { - fun buildBuiltInLaunchProfiles(project: Project?): List -} - -@ApiStatus.Internal -object AgentSessionLaunchProfileContributors { - private val EP = ExtensionPointName("com.intellij.agent.workbench.sessionLaunchProfileContributor") - - fun buildBuiltInLaunchProfiles(project: Project?): List { - return if (EP.hasAnyExtensions()) EP.extensionList.flatMap { contributor -> contributor.buildBuiltInLaunchProfiles(project) } else emptyList() - } -} - -data class AgentSessionLaunchProfileSnapshot( - @JvmField val builtInProfiles: List, - @JvmField val userProfiles: List, -) { - val allProfiles: List - get() = effectiveLaunchProfiles(builtInProfiles, userProfiles) -} - -@ApiStatus.Internal -data class AgentSessionResolvedLaunchProfile( - @JvmField val id: String, - @JvmField val profile: AgentPromptLaunchProfile, - val provider: AgentSessionProvider, - @JvmField val launchMode: AgentSessionLaunchMode, - @JvmField val launchTargetId: String?, - val surfaceId: AgentSessionSurfaceId, - @JvmField val generationSettings: AgentPromptGenerationSettings, -) - -@ApiStatus.Internal -interface AgentSessionLaunchProfileResolver { - fun resolveLaunchProfile( - launchProfileId: String?, - requiredProvider: AgentSessionProvider? = null, - ): AgentSessionResolvedLaunchProfile? -} - -@ApiStatus.Internal -fun resolveAgentSessionLaunchProfile( - launchProfileId: String?, - requiredProvider: AgentSessionProvider? = null, - defaultProfileId: String? = null, - builtInProfiles: List, - userProfiles: List, - providerDescriptors: Iterable, -): AgentSessionResolvedLaunchProfile? { - val descriptorsByProvider = providerDescriptors.associateBy { descriptor -> descriptor.provider.value } - val effectiveProfiles = effectiveLaunchProfiles(builtInProfiles, userProfiles) - - fun resolve(profile: AgentPromptLaunchProfile): AgentSessionResolvedLaunchProfile? { - val normalizedProfile = normalizeLaunchProfileForResolution(profile) - val provider = AgentSessionProvider.fromOrNull(normalizedProfile.providerId) ?: return null - if (requiredProvider != null && provider != requiredProvider) return null - val descriptor = descriptorsByProvider[provider.value] ?: return null - if (normalizedProfile.launchMode !in descriptor.supportedLaunchModes) return null - return AgentSessionResolvedLaunchProfile( - id = normalizedProfile.id, - profile = normalizedProfile, - provider = provider, - launchMode = normalizedProfile.launchMode, - launchTargetId = normalizedProfile.launchTargetId, - surfaceId = effectiveAgentSessionSurfaceId(descriptor, normalizedProfile.surfaceId), - generationSettings = descriptor.sanitizeGenerationSettings(normalizedProfile.generationSettings), - ) - } - - val normalizedProfileId = launchProfileId?.trim()?.takeIf(String::isNotEmpty) - if (normalizedProfileId != null) { - effectiveProfiles.firstOrNull { profile -> profile.id == normalizedProfileId }?.let { profile -> - resolve(profile)?.let { resolved -> return resolved } - } - } - - val normalizedDefaultProfileId = defaultProfileId?.trim()?.takeIf(String::isNotEmpty) - if (normalizedDefaultProfileId != null && normalizedDefaultProfileId != normalizedProfileId) { - effectiveProfiles.firstOrNull { profile -> profile.id == normalizedDefaultProfileId }?.let { profile -> - resolve(profile)?.let { resolved -> return resolved } - } - } - - val compatibleProfiles = effectiveProfiles.mapNotNull(::resolve) - if (requiredProvider != null) { - compatibleProfiles.firstOrNull { profile -> - profile.id == builtInLaunchProfileId(requiredProvider, AgentSessionLaunchMode.STANDARD) - }?.let { profile -> return profile } - } - return compatibleProfiles.firstOrNull { profile -> profile.launchMode == AgentSessionLaunchMode.STANDARD } - ?: compatibleProfiles.firstOrNull() -} - -fun effectiveLaunchProfiles( - builtInProfiles: List, - userProfiles: List, -): List { - val userProfilesById = LinkedHashMap() - userProfiles.forEach { profile -> userProfilesById[profile.id] = profile } - val builtInProfileIds = builtInProfiles.mapTo(HashSet()) { profile -> profile.id } - return builtInProfiles.map { profile -> userProfilesById[profile.id] ?: profile } + - userProfiles.filter { profile -> profile.id !in builtInProfileIds } -} - -fun launchProfileEditablePayload(profile: AgentPromptLaunchProfile): AgentPromptLaunchProfile { - return profile.copy( - id = "", - kind = AgentPromptLaunchProfileKind.USER, - ) -} - -fun launchProfileMatchesBuiltIn( - profile: AgentPromptLaunchProfile, - builtInProfile: AgentPromptLaunchProfile, -): Boolean { - return launchProfileEditablePayload(profile) == launchProfileEditablePayload(builtInProfile) -} - -fun normalizedUserLaunchProfile(profile: AgentPromptLaunchProfile): AgentPromptLaunchProfile { - return profile.copy( - kind = AgentPromptLaunchProfileKind.USER, - ) -} - -fun builtInLaunchProfileId(provider: AgentSessionProvider, launchMode: AgentSessionLaunchMode): String { - return "$BUILT_IN_LAUNCH_PROFILE_PREFIX${provider.value}:${launchMode.name.lowercase()}" -} - -@ApiStatus.Internal -fun builtInLaunchTargetProfileId( - provider: AgentSessionProvider, - launchTargetId: String, - launchMode: AgentSessionLaunchMode, -): String { - val encodedTargetId = Base64.getUrlEncoder().withoutPadding().encodeToString(launchTargetId.toByteArray(StandardCharsets.UTF_8)) - return "$BUILT_IN_LAUNCH_PROFILE_PREFIX${provider.value}:target:$encodedTargetId:${launchMode.name.lowercase()}" -} - -@ApiStatus.Internal -fun buildBuiltInLaunchProfiles( - menuModel: AgentSessionProviderMenuModel, - resolveName: (AgentSessionProviderMenuItem) -> String, - project: Project? = null, -): List { - val providerProfiles = (menuModel.standardItems + menuModel.yoloItems) - .filter(AgentSessionProviderMenuItem::isEnabled) - .filter { item -> item.bridge.supportsDefaultLaunchProfile } - .map { item -> - AgentPromptLaunchProfile( - id = builtInLaunchProfileId(item.bridge.provider, item.mode), - name = resolveName(item), - kind = AgentPromptLaunchProfileKind.BUILT_IN, - providerId = item.bridge.provider.value, - launchMode = item.mode, - ) - } - val contributedProfiles = AgentSessionLaunchProfileContributors.buildBuiltInLaunchProfiles(project).map { profile -> - AgentPromptLaunchProfile( - id = profile.id, - name = profile.name, - kind = AgentPromptLaunchProfileKind.BUILT_IN, - providerId = profile.provider.value, - launchMode = profile.launchMode, - launchTargetId = profile.launchTargetId, - surfaceId = profile.surfaceId?.value, - ) - } - return (providerProfiles + contributedProfiles).distinctBy(AgentPromptLaunchProfile::id) -} - -fun generationSettingsForPlanMode( - generationSettings: AgentPromptGenerationSettings, - startInPlanMode: Boolean, -): AgentPromptGenerationSettings { - if (!startInPlanMode) { - return generationSettings.copy(planReasoningEffort = null) - } - return generationSettings -} - -fun initialMessageRequestForLaunchProfile(@Suppress("UNUSED_PARAMETER") profile: AgentPromptLaunchProfile): AgentPromptInitialMessageRequest { - return AgentPromptInitialMessageRequest(prompt = "") -} - -private fun normalizeLaunchProfileForResolution(profile: AgentPromptLaunchProfile): AgentPromptLaunchProfile { - val launchTargetId = profile.launchTargetId?.trim()?.takeIf(String::isNotEmpty) - val surfaceId = normalizeAgentSessionSurfaceId(profile.surfaceId) - if (launchTargetId != null) { - return profile.copy( - launchTargetId = launchTargetId, - surfaceId = surfaceId, - ) - } - - if (profile.providerId == ACP_PROVIDER_ID) { - val legacyAgentKey = profile.generationSettings.modelId?.trim()?.takeIf(String::isNotEmpty) - if (legacyAgentKey != null) { - return profile.copy( - launchTargetId = legacyAgentKey, - surfaceId = surfaceId ?: AgentSessionSurfaces.ACP.value, - generationSettings = profile.generationSettings.copy(modelId = null), - ) - } - } - - if (profile.launchTargetId == null && profile.surfaceId == surfaceId) { - return profile - } - return profile.copy( - launchTargetId = null, - surfaceId = surfaceId, - ) -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionProviderDescriptor.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionProviderDescriptor.kt deleted file mode 100644 index 737b321ac83b..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionProviderDescriptor.kt +++ /dev/null @@ -1,607 +0,0 @@ -// 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.providers - -// @spec plugins/air/spec/sessions/agent-terminal-sessions.spec.md - -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.parseAgentThreadIdentity -import com.intellij.platform.ai.agent.sessions.core.isAgentSessionPendingThreadId -import com.intellij.agent.workbench.prompt.core.AgentPromptContextEnvelopeFormatter -import com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings -import com.intellij.agent.workbench.prompt.core.AgentPromptGenerationModel -import com.intellij.agent.workbench.prompt.core.AgentPromptInitialMessageRequest -import com.intellij.agent.workbench.prompt.core.AgentPromptReasoningEffort -import com.intellij.agent.workbench.prompt.core.AgentPromptReusableSourceEntry -import com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionSurfaceId -import com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionSurfaces -import com.intellij.openapi.project.Project -import org.jetbrains.annotations.ApiStatus -import java.nio.file.Path -import javax.swing.Icon -import javax.swing.JComponent - -enum class AgentInitialMessageStartupPolicy { - TRY_STARTUP_COMMAND, - POST_START_ONLY, -} - -enum class AgentInitialMessageMode { - STANDARD, - PLAN, -} - -enum class AgentInitialMessageTimeoutPolicy { - ALLOW_TIMEOUT_FALLBACK, - REQUIRE_EXPLICIT_READINESS, -} - -enum class AgentInitialMessageDispatchAction { - SEND_TEXT, - PROVIDER, -} - -data class AgentInitialMessageProviderDispatchRequest( - @JvmField val project: Project, - @JvmField val projectPath: String, - @JvmField val threadId: String, - @JvmField val message: String, - @JvmField val mode: AgentInitialMessageMode, - @JvmField val generationSettings: AgentPromptGenerationSettings, -) - -/** - * Controls how synchronous provider pickers behave before CLI availability is known. - */ -enum class AgentSessionProviderCliVisibilityPolicy { - PROMINENT, - DISCOVER_WHEN_AVAILABLE, -} - -data class AgentSessionMenuCommand( - @JvmField val command: String, - @JvmField val argumentHint: String = "", -) - -data class AgentSessionPromptCommandCompletionEntry( - @JvmField val command: String, - @JvmField val kind: AgentSessionPromptCommandCompletionKind, - @JvmField val sourceKey: String, - @JvmField val sourcePath: Path? = null, - @JvmField val argumentHint: String = "", -) - -enum class AgentSessionPromptCommandCompletionKind { - MENU, - COMMAND, - SKILL, -} - -typealias AgentThreadRenameAction = suspend (path: String, threadId: String, normalizedName: String) -> Boolean - -data class AgentInitialMessagePlan( - @JvmField val message: String?, - @JvmField val mode: AgentInitialMessageMode = AgentInitialMessageMode.STANDARD, - @JvmField val startupPolicy: AgentInitialMessageStartupPolicy = AgentInitialMessageStartupPolicy.TRY_STARTUP_COMMAND, - @JvmField val timeoutPolicy: AgentInitialMessageTimeoutPolicy = AgentInitialMessageTimeoutPolicy.ALLOW_TIMEOUT_FALLBACK, -) { - companion object { - @JvmField - val EMPTY: AgentInitialMessagePlan = AgentInitialMessagePlan(message = null) - - fun composeDefault(request: AgentPromptInitialMessageRequest): AgentInitialMessagePlan { - val message = AgentPromptContextEnvelopeFormatter.composeInitialMessage(request) - .trim() - .takeIf { it.isNotEmpty() } - return AgentInitialMessagePlan(message = message) - } - } -} - -data class AgentInitialMessageDispatchStep( - @JvmField val text: String = "", - @JvmField val timeoutPolicy: AgentInitialMessageTimeoutPolicy = AgentInitialMessageTimeoutPolicy.ALLOW_TIMEOUT_FALLBACK, - @JvmField val action: AgentInitialMessageDispatchAction = AgentInitialMessageDispatchAction.SEND_TEXT, - @JvmField val recordsPrompt: Boolean = true, -) { - fun isDispatchable(): Boolean { - return text.isNotBlank() - } -} - -enum class AgentInitialPromptDeliveryStatus { - PENDING, - DELIVERED, -} - -enum class AgentInitialPromptDeliveryChannel { - STARTUP_COMMAND, - TERMINAL, - APP_SERVER, -} - -data class AgentInitialPromptRecord( - @JvmField val message: String?, - @JvmField val mode: AgentInitialMessageMode = AgentInitialMessageMode.STANDARD, - @JvmField val token: String? = null, - @JvmField val deliveryStatus: AgentInitialPromptDeliveryStatus = AgentInitialPromptDeliveryStatus.PENDING, - @JvmField val deliveryChannel: AgentInitialPromptDeliveryChannel? = null, -) - -data class AgentTerminalPromptDispatch( - @JvmField val steps: List, - @JvmField val stepIndex: Int = 0, -) { - fun normalized(): AgentTerminalPromptDispatch? { - val normalizedSteps = steps.filter(AgentInitialMessageDispatchStep::isDispatchable) - if (normalizedSteps.isEmpty()) return null - return AgentTerminalPromptDispatch( - steps = normalizedSteps, - stepIndex = stepIndex.coerceIn(0, normalizedSteps.size), - ) - } -} - -data class AgentInitialPromptDeliveryPlan( - @JvmField val startupLaunchSpecOverride: AgentSessionTerminalLaunchSpec? = null, - @JvmField val promptRecord: AgentInitialPromptRecord? = null, - @JvmField val terminalDispatch: AgentTerminalPromptDispatch? = null, - @JvmField val startupFallbackTerminalDispatch: AgentTerminalPromptDispatch? = null, -) { - constructor( - startupLaunchSpecOverride: AgentSessionTerminalLaunchSpec? = null, - postStartDispatchSteps: List, - initialMessageToken: String? = null, - ) : this( - startupLaunchSpecOverride = startupLaunchSpecOverride, - promptRecord = buildPromptRecord( - steps = postStartDispatchSteps, - token = initialMessageToken, - deliveredByStartupCommand = startupLaunchSpecOverride != null, - ), - terminalDispatch = AgentTerminalPromptDispatch( - steps = postStartDispatchSteps, - ).normalized().takeIf { startupLaunchSpecOverride == null }, - startupFallbackTerminalDispatch = AgentTerminalPromptDispatch( - steps = postStartDispatchSteps, - ).normalized().takeIf { startupLaunchSpecOverride != null }, - ) - - val postStartDispatchSteps: List - get() = terminalDispatch?.steps.orEmpty() - - val initialMessageToken: String? - get() = promptRecord?.token - - fun withStartupDeliveryIgnored(): AgentInitialPromptDeliveryPlan { - val record = promptRecord ?: return EMPTY - if (startupLaunchSpecOverride == null) { - return this - } - val fallbackDispatch = terminalDispatch ?: startupFallbackTerminalDispatch ?: buildTerminalDispatch(record) ?: return EMPTY - return AgentInitialPromptDeliveryPlan( - promptRecord = record.copy( - deliveryStatus = AgentInitialPromptDeliveryStatus.PENDING, - deliveryChannel = AgentInitialPromptDeliveryChannel.TERMINAL, - ), - terminalDispatch = fallbackDispatch, - ) - } - - companion object { - @JvmField - val EMPTY: AgentInitialPromptDeliveryPlan = AgentInitialPromptDeliveryPlan() - } -} - -private fun buildTerminalDispatch(promptRecord: AgentInitialPromptRecord): AgentTerminalPromptDispatch? { - val message = promptRecord.message?.trim()?.takeIf { it.isNotEmpty() } ?: return null - if (promptRecord.mode == AgentInitialMessageMode.PLAN) { - return null - } - val steps = listOf(AgentInitialMessageDispatchStep(text = message)) - return AgentTerminalPromptDispatch(steps = steps).normalized() -} - -private fun buildPromptRecord( - steps: List, - token: String?, - deliveredByStartupCommand: Boolean, -): AgentInitialPromptRecord? { - val message = steps.lastOrNull { step -> - step.recordsPrompt && - step.action == AgentInitialMessageDispatchAction.SEND_TEXT && - step.text.isNotBlank() - }?.text ?: return null - return AgentInitialPromptRecord( - message = message, - token = token, - deliveryStatus = if (deliveredByStartupCommand) { - AgentInitialPromptDeliveryStatus.DELIVERED - } - else { - AgentInitialPromptDeliveryStatus.PENDING - }, - deliveryChannel = if (deliveredByStartupCommand) { - AgentInitialPromptDeliveryChannel.STARTUP_COMMAND - } - else { - AgentInitialPromptDeliveryChannel.TERMINAL - }, - ) -} - -data class AgentPrestartedNewSessionLaunch( - @JvmField val launchSpec: AgentSessionTerminalLaunchSpec, - @JvmField val initialMessageDispatchPlan: AgentInitialPromptDeliveryPlan? = null, -) - -data class AgentPrestartNewSessionLaunchRequest( - @JvmField val projectPath: String, - @JvmField val launchMode: AgentSessionLaunchMode, - @JvmField val initialMessagePlan: AgentInitialMessagePlan, - @JvmField val generationSettings: AgentPromptGenerationSettings, - @JvmField val generationModelCatalog: List, - @JvmField val launchSpec: AgentSessionTerminalLaunchSpec, -) - -data class AgentPendingSessionMetadata( - @JvmField val createdAtMs: Long, - @JvmField val launchMode: String?, -) - -@ApiStatus.Internal -interface AgentSessionProviderDescriptor { - val provider: AgentSessionProvider - - val displayNameKey: String - val displayNameFallback: String - get() = provider.value.replaceFirstChar { char -> - if (char.isLowerCase()) char.titlecase() else char.toString() - } - val cliDisplayNameKey: String - get() = displayNameKey - val cliDisplayNameFallback: String - get() = displayNameFallback - val displayPriority: Int - get() = Int.MAX_VALUE - val newSessionLabelKey: String - val newSessionTitleKey: String - get() = "toolwindow.action.new.thread" - val quickStartLabelKey: String - get() = newSessionLabelKey - val quickStartActionTextKey: String - get() = "action.AgentWorkbenchSessions.NewThreadQuick.text" - val quickStartActionDescriptionKey: String - get() = "action.AgentWorkbenchSessions.NewThreadQuick.description" - val quickStartActionTargetDescriptionKey: String? - get() = null - val newSessionDescriptionKey: String? - get() = null - val yoloSessionLabelKey: String? - get() = null - val yoloSessionModeLabelKey: String? - get() = null - val icon: Icon - - /** Desaturated variant for persistent surfaces (tree, toolbar button, tabs, status bar); menus keep [icon]. */ - val monochromeIcon: Icon - get() = icon - val promptOptions: List - get() = emptyList() - - /** - * Concrete reasoning efforts accepted by this provider. [AgentPromptReasoningEffort.AUTO] is implicit and means - * no launch override should be added. - */ - val supportedReasoningEfforts: Set - get() = emptySet() - - /** - * True when the provider has a dedicated Plan-mode reasoning effort transport separate from normal reasoning effort. - */ - val supportsPlanReasoningEffort: Boolean - get() = false - - val supportsGenerationModelSelection: Boolean - get() = false - - /** - * True when provider model discovery should run even if the launch profile uses automatic generation settings. - */ - val resolvesGenerationModelCatalogForAutoSettings: Boolean - get() = false - - val supportsPromptLaunch: Boolean - get() = true - - /** - * True when the provider itself can be launched as a built-in profile without an additional target. - */ - val supportsDefaultLaunchProfile: Boolean - get() = supportsPromptLaunch - - val supportedLaunchModes: Set - get() = setOf(AgentSessionLaunchMode.STANDARD) - - val defaultLaunchSurface: AgentSessionSurfaceId - get() = AgentSessionSurfaces.TERMINAL - - val supportedLaunchSurfaces: Set - get() = setOf(defaultLaunchSurface) - - val supportsPromptTabQueueShortcut: Boolean - get() = false - - val suppressPromptExistingTaskSelectionHint: Boolean - get() = false - - val supportsPendingEditorTabRebind: Boolean - get() = false - - val supportsNewThreadRebind: Boolean - get() = false - - /** - * Whether [sessionSource] emits path/thread-scoped refresh events precise enough for active threadView tabs and outlines. - */ - val emitsScopedRefreshSignals: Boolean - get() = false - - val refreshPathAfterCreateNewSession: Boolean - get() = false - - val archiveRefreshDelayMs: Long - get() = 0L - - val suppressArchivedThreadsDuringRefresh: Boolean - get() = false - - /** - * Provider-backed session source. - * - * The required source contract is active-thread listing through [AgentSessionSource.listThreads]. Optional behaviors such as archived - * rows, refresh events, cost hydration, and thread outlines are exposed by the focused `AgentSession*Source` capability interfaces. - * Providers that support thread outlines must follow the role-aware [AgentSessionThreadOutlineSource.loadThreadOutline] contract so - * shared threadView UI can render user prompts, assistant responses, and tool activity consistently across providers. - */ - val sessionSource: AgentSessionSource - val cliMissingMessageKey: String - - val cliVisibilityPolicy: AgentSessionProviderCliVisibilityPolicy - get() = AgentSessionProviderCliVisibilityPolicy.PROMINENT - - /** - * Terminal-agent identifier used by `TerminalAgentResolver` to locate this provider's CLI binary. - * The provider availability cache refreshes through [isCliAvailable], which should use the same - * resolver path as launch-time CLI resolution so menus and launch errors agree. - */ - val terminalAgentKey: String? - get() = null - - val supportsArchiveThread: Boolean - get() = false - - /** - * True when closing the last editor tab for a concrete threadView should archive the provider thread. - * This is separate from [supportsArchiveThread], which also covers explicit user-initiated archive actions. - */ - val archiveOnLastEditorClose: Boolean - get() = false - - /** - * Close any open threadView tab before invoking [archiveThread]. Use this for providers whose archive transport resumes - * the session non-interactively and cannot safely run while the same session is open in a terminal. - */ - val closeOpenThreadViewBeforeArchiveThread: Boolean - get() = false - - /** - * Provider-side rename implementation. Implementations should only persist or perform the provider rename and report - * success. Agent Workbench owns session-state updates, shared thread presentation, open editor-tab presentation updates, - * and follow-up refreshes. - */ - val threadRenameAction: AgentThreadRenameAction? - get() = null - - val supportsUnarchiveThread: Boolean - get() = false - - /** - * Resolves whether the provider's CLI binary is available, using the same shared `TerminalAgentResolver` - * pipeline that powers terminal launches. The result reflects PATH + known-location candidates so - * menu enable/disable matches the launch-time resolver answer. - * - * Synchronous UI surfaces (menu `update()` callbacks, sessions tree popup, editor-tab actions, etc.) - * cannot suspend, and so consume the project-level provider availability cache instead of calling - * this directly. The cache is populated by startup prewarm and refreshed from background coroutines. - */ - suspend fun isCliAvailable(): Boolean - - suspend fun buildResumeLaunchSpec(sessionId: String): AgentSessionTerminalLaunchSpec - - suspend fun buildResumeLaunchSpec( - sessionId: String, - launchMode: AgentSessionLaunchMode, - ): AgentSessionTerminalLaunchSpec = buildResumeLaunchSpec(sessionId) - - suspend fun listAvailableGenerationModels(project: Project? = null): List { - return emptyList() - } - - fun displayNameForGenerationModelId(modelId: String): String? { - return null - } - - suspend fun buildNewSessionLaunchSpec(mode: AgentSessionLaunchMode): AgentSessionTerminalLaunchSpec - - suspend fun prestartNewSessionLaunch(request: AgentPrestartNewSessionLaunchRequest): AgentPrestartedNewSessionLaunch? = null - - fun sanitizeGenerationSettings(generationSettings: AgentPromptGenerationSettings): AgentPromptGenerationSettings { - val modelId = generationSettings.modelId - ?.trim() - ?.takeIf { it.isNotEmpty() } - val reasoningEffort = generationSettings.reasoningEffort - .takeIf { effort -> effort == AgentPromptReasoningEffort.AUTO || effort in supportedReasoningEfforts } - ?: AgentPromptReasoningEffort.AUTO - val planReasoningEffort = if (supportsPlanReasoningEffort) { - generationSettings.planReasoningEffort - ?.takeIf { effort -> effort == AgentPromptReasoningEffort.AUTO || effort in supportedReasoningEfforts } - ?: generationSettings.planReasoningEffort?.let { AgentPromptReasoningEffort.AUTO } - } - else { - null - } - return generationSettings.copy( - modelId = modelId, - reasoningEffort = reasoningEffort, - planReasoningEffort = planReasoningEffort, - ) - } - - fun applyGenerationSettings( - baseLaunchSpec: AgentSessionTerminalLaunchSpec, - generationSettings: AgentPromptGenerationSettings, - initialMessagePlan: AgentInitialMessagePlan, - ): AgentSessionTerminalLaunchSpec = baseLaunchSpec - - fun applyGenerationModelCatalog( - baseLaunchSpec: AgentSessionTerminalLaunchSpec, - generationSettings: AgentPromptGenerationSettings, - generationModelCatalog: List, - ): AgentSessionTerminalLaunchSpec = baseLaunchSpec - - fun buildLaunchSpecWithInitialMessage( - baseLaunchSpec: AgentSessionTerminalLaunchSpec, - initialMessagePlan: AgentInitialMessagePlan, - ): AgentSessionTerminalLaunchSpec? = null - - fun buildPostStartDispatchSteps(initialMessagePlan: AgentInitialMessagePlan): List { - val message = initialMessagePlan.message ?: return emptyList() - return listOf( - AgentInitialMessageDispatchStep( - text = message, - timeoutPolicy = initialMessagePlan.timeoutPolicy, - ) - ) - } - - suspend fun dispatchInitialMessageToProvider(request: AgentInitialMessageProviderDispatchRequest): Boolean = false - - suspend fun archiveThread(path: String, threadId: String): Boolean = false - - suspend fun unarchiveThread(path: String, threadId: String): Boolean = false - - fun buildInitialMessagePlan(request: AgentPromptInitialMessageRequest): AgentInitialMessagePlan - - /** - * True when [buildInitialMessagePlan] depends on data refreshed by [isCliAvailable]. - */ - val requiresCliAvailabilityForInitialMessagePlan: Boolean - get() = false - - suspend fun listReusablePromptSourceEntries(projectPath: String): List { - return emptyList() - } - - val menuCommands: List - get() = emptyList() - - fun isMenuCommandPrompt(prompt: String): Boolean { - val token = prompt.leadingSlashCommandToken() ?: return false - return menuCommands.any { command -> command.command == token } - } - - fun collectPromptCommandCompletionEntries(projectPaths: Iterable): List { - return menuCommands.map { command -> - AgentSessionPromptCommandCompletionEntry( - command = command.command, - kind = AgentSessionPromptCommandCompletionKind.MENU, - sourceKey = command.command, - argumentHint = command.argumentHint, - ) - } - } - - fun onThreadViewOpened() { - } - - fun recordNewSession(path: String, threadId: String, title: String, createdAtMs: Long) { - } - - val supportsTerminalRestoreContext: Boolean - get() = false - - fun readTerminalRestoreContext(path: String, threadId: String): AgentSessionTerminalRestoreContext? = null - - fun recordTerminalWorkingDirectory(path: String, threadId: String, workingDirectory: String) { - } - - fun recordTerminalSessionClosed(path: String, threadId: String) { - } - - /** - * CLI-arg marker that distinguishes a YOLO-mode launch from a standard one for this provider. - * The default [AgentSessionProviderDescriptor.resolvePendingSessionMetadata] implementation looks for this token in - * [AgentSessionTerminalLaunchSpec.command] to label the launch mode. `null` (the default) - * means YOLO is not distinguishable from the launch spec — every pending session is reported - * as `"standard"`. - */ - val pendingSessionLaunchYoloMarker: String? - get() = null - - fun createToolWindowNorthComponent(project: Project): JComponent? = null - - fun shouldStripContextForPrompt(prompt: String): Boolean = isMenuCommandPrompt(prompt) - - fun isCliMissingError(throwable: Throwable): Boolean = false - - fun resolvePendingSessionMetadata( - identity: String, - launchSpec: AgentSessionTerminalLaunchSpec, - ): AgentPendingSessionMetadata? { - val parsed = parseAgentThreadIdentity(identity) ?: return null - if (!isAgentSessionPendingThreadId(parsed.threadId)) return null - if (parsed.providerId != provider.value) return null - val yoloMarker = pendingSessionLaunchYoloMarker - val launchMode = if (yoloMarker != null && yoloMarker in launchSpec.command) "yolo" else "standard" - return AgentPendingSessionMetadata(createdAtMs = System.currentTimeMillis(), launchMode = launchMode) - } -} - -private fun String.leadingSlashCommandToken(): String? { - val normalized = trimStart() - if (!normalized.startsWith('/')) { - return null - } - return normalized.takeWhile { char -> !char.isWhitespace() } -} - -data class AgentSessionTerminalLaunchSpec( - @JvmField val command: List, - @JvmField val envVariables: Map = emptyMap(), - @JvmField val workingDirectory: String? = null, - /** - * Uses the IDE Terminal default shell instead of an explicit non-shell command. - * The embedded Agent Thread View terminal passes no `shellCommand` to the Terminal tab builder in this mode. - */ - @JvmField val useTerminalDefaultShell: Boolean = false, - /** - * When set, this session targets a Docker container managed by `ContainerSessionManager`. - * The ij-proxy MCP server routes file I/O and bash tool calls through the container - * instead of the local filesystem. Semantic tools (search_symbol, get_file_problems) - * fall back to the host IDE index. - * - * The ID is also passed to Claude Code as `AGENT_CONTAINER_SESSION_ID` env var. - */ - @JvmField val containerSessionId: String? = null, - /** - * Provider-allocated concrete session id for new-session launches. - * When set, Agent Thread View opens the tab with the concrete identity immediately instead of a synthetic `new-*` id. - */ - @JvmField val preallocatedSessionId: String? = null, -) - -data class AgentSessionTerminalRestoreContext( - @JvmField val workingDirectory: String? = null, -) diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionProviderFeatureSettings.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionProviderFeatureSettings.kt deleted file mode 100644 index 18d0282d8ed4..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionProviderFeatureSettings.kt +++ /dev/null @@ -1,25 +0,0 @@ -// 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.providers - -import com.intellij.platform.ai.agent.core.session.AgentSessionProvider -import com.intellij.openapi.extensions.ExtensionPointName - -interface AgentSessionProviderFeatureSettings { - fun isProviderFeatureEnabled(provider: AgentSessionProvider, featureId: String): Boolean = true - - fun setProviderFeatureEnabled(provider: AgentSessionProvider, featureId: String, enabled: Boolean) { - } -} - -object AgentSessionProviderFeatureSettingsExtensions { - val EP_NAME: ExtensionPointName = - ExtensionPointName("com.intellij.agent.workbench.sessionProviderFeatureSettings") - - fun isProviderFeatureEnabled(provider: AgentSessionProvider, featureId: String): Boolean { - return EP_NAME.extensionList.firstOrNull()?.isProviderFeatureEnabled(provider, featureId) ?: true - } - - fun setProviderFeatureEnabled(provider: AgentSessionProvider, featureId: String, enabled: Boolean) { - EP_NAME.extensionList.firstOrNull()?.setProviderFeatureEnabled(provider, featureId, enabled) - } -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionProviderMenuModel.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionProviderMenuModel.kt deleted file mode 100644 index 588497c575f8..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionProviderMenuModel.kt +++ /dev/null @@ -1,83 +0,0 @@ -// 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.providers - -import com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode -import com.intellij.platform.ai.agent.core.session.AgentSessionProvider -import org.jetbrains.annotations.ApiStatus - -@ApiStatus.Internal -data class AgentSessionProviderMenuModel( - @JvmField val standardItems: List, - @JvmField val yoloItems: List, -) - -@ApiStatus.Internal -data class AgentSessionProviderMenuItem( - @JvmField val bridge: AgentSessionProviderDescriptor, - @JvmField val mode: AgentSessionLaunchMode, - @JvmField val labelKey: String, - @JvmField val isEnabled: Boolean, - @JvmField val disabledReasonKey: String? = null, -) - -@ApiStatus.Internal -fun AgentSessionProviderMenuModel.hasEntries(): Boolean { - return standardItems.isNotEmpty() || yoloItems.isNotEmpty() -} - -/** - * Sync menu-model builder. Synchronous surfaces (action `update()` callbacks) must supply - * [availabilityByProvider] from the caller's project-level provider availability cache. - */ -@ApiStatus.Internal -fun buildAgentSessionProviderMenuModel( - bridges: List, - availabilityByProvider: Map, -): AgentSessionProviderMenuModel { - val standardItems = ArrayList(bridges.size) - val yoloItems = ArrayList() - - bridges.forEach { bridge -> - val cliAvailable = availabilityByProvider[bridge.provider] == true - appendMenuItems(bridge, cliAvailable, standardItems, yoloItems) - } - - return AgentSessionProviderMenuModel( - standardItems = standardItems, - yoloItems = yoloItems, - ) -} - -private fun appendMenuItems( - bridge: AgentSessionProviderDescriptor, - cliAvailable: Boolean, - standardItems: MutableList, - yoloItems: MutableList, -) { - if (!cliAvailable && bridge.cliVisibilityPolicy == AgentSessionProviderCliVisibilityPolicy.DISCOVER_WHEN_AVAILABLE) { - return - } - - val disabledReasonKey = if (cliAvailable) null else bridge.cliMissingMessageKey - - if (AgentSessionLaunchMode.STANDARD in bridge.supportedLaunchModes) { - standardItems += AgentSessionProviderMenuItem( - bridge = bridge, - mode = AgentSessionLaunchMode.STANDARD, - labelKey = bridge.newSessionLabelKey, - isEnabled = cliAvailable, - disabledReasonKey = disabledReasonKey, - ) - } - - val yoloLabelKey = bridge.yoloSessionLabelKey - if (yoloLabelKey != null && AgentSessionLaunchMode.YOLO in bridge.supportedLaunchModes) { - yoloItems += AgentSessionProviderMenuItem( - bridge = bridge, - mode = AgentSessionLaunchMode.YOLO, - labelKey = yoloLabelKey, - isEnabled = cliAvailable, - disabledReasonKey = disabledReasonKey, - ) - } -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionProviderPlanMode.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionProviderPlanMode.kt deleted file mode 100644 index c30ded90e25e..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionProviderPlanMode.kt +++ /dev/null @@ -1,50 +0,0 @@ -// 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.providers - -import com.intellij.platform.ai.agent.core.AgentThreadActivity -import com.intellij.agent.workbench.prompt.core.AgentPromptInitialMessageRequest -import org.jetbrains.annotations.ApiStatus - -fun AgentPromptInitialMessageRequest.isPlanModeRequested(): Boolean { - return AGENT_PROMPT_PROVIDER_OPTION_PLAN_MODE in providerOptionIds -} - -fun AgentThreadActivity.isBusyForExistingThreadPlanMode(): Boolean { - return this == AgentThreadActivity.PROCESSING || this == AgentThreadActivity.REVIEWING -} - -fun AgentInitialMessagePlan.isBlockedForExistingThreadPlanMode(threadActivity: AgentThreadActivity?): Boolean { - return mode == AgentInitialMessageMode.PLAN && threadActivity?.isBusyForExistingThreadPlanMode() == true -} - -@ApiStatus.Internal -fun AgentSessionProviderDescriptor.isPlanModeBlockedForExistingThread( - request: AgentPromptInitialMessageRequest, - threadActivity: AgentThreadActivity?, -): Boolean { - return buildInitialMessagePlan(request).isBlockedForExistingThreadPlanMode(threadActivity) -} - -fun buildPlanModeInitialMessagePlan( - request: AgentPromptInitialMessageRequest, - startupPolicyWhenPlanModeEnabled: AgentInitialMessageStartupPolicy, - startupPolicyWhenPlanModeDisabled: AgentInitialMessageStartupPolicy = AgentInitialMessageStartupPolicy.TRY_STARTUP_COMMAND, -): AgentInitialMessagePlan { - val basePlan = AgentInitialMessagePlan.composeDefault(request) - val planMode = request.isPlanModeRequested() - val normalizedMessage = basePlan.message - if (!planMode && normalizedMessage == null) { - return basePlan - } - return AgentInitialMessagePlan( - message = normalizedMessage, - mode = if (planMode) AgentInitialMessageMode.PLAN else AgentInitialMessageMode.STANDARD, - startupPolicy = if (planMode) startupPolicyWhenPlanModeEnabled else startupPolicyWhenPlanModeDisabled, - timeoutPolicy = if (planMode) { - AgentInitialMessageTimeoutPolicy.REQUIRE_EXPLICIT_READINESS - } - else { - AgentInitialMessageTimeoutPolicy.ALLOW_TIMEOUT_FALLBACK - }, - ) -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionProviderUiContributor.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionProviderUiContributor.kt deleted file mode 100644 index d91542f86224..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionProviderUiContributor.kt +++ /dev/null @@ -1,56 +0,0 @@ -// 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.providers - -import com.intellij.platform.ai.agent.core.session.AgentSessionProvider -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.extensions.ExtensionPointName -import com.intellij.openapi.extensions.RequiredElement -import com.intellij.openapi.project.Project -import com.intellij.serviceContainer.BaseKeyedLazyInstance -import com.intellij.util.xmlb.annotations.Attribute -import javax.swing.JComponent - -private class AgentSessionProviderUiContributorRegistryLog - -private val UI_CONTRIBUTOR_LOG = logger() - -interface AgentSessionProviderUiContributor { - fun onThreadViewOpened() { - } - - fun createToolWindowNorthComponent(project: Project): JComponent? = null -} - -class AgentSessionProviderUiContributorBean : BaseKeyedLazyInstance() { - @Attribute("providerId") - @JvmField - @RequiredElement - var providerId: String = "" - - @Attribute("implementation") - @JvmField - @RequiredElement - var implementation: String = "" - - override fun getImplementationClassName(): String = implementation - - fun matches(provider: AgentSessionProvider): Boolean { - val contributedProvider = AgentSessionProvider.fromOrNull(providerId) - if (contributedProvider == null) { - UI_CONTRIBUTOR_LOG.warn("Ignoring session provider UI contributor with invalid providerId '$providerId': $implementation") - return false - } - return contributedProvider == provider - } -} - -object AgentSessionProviderUiContributors { - val EP_NAME: ExtensionPointName = - ExtensionPointName("com.intellij.agent.workbench.sessionProviderUiContributor") - - fun forProvider(provider: AgentSessionProvider): List { - return EP_NAME.extensionList.mapNotNull { contributorBean -> - if (contributorBean.matches(provider)) contributorBean.instance else null - } - } -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionProviders.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionProviders.kt deleted file mode 100644 index b710561ccdef..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionProviders.kt +++ /dev/null @@ -1,148 +0,0 @@ -// 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.providers - -import com.intellij.platform.ai.agent.core.extensions.OverridableValue -import com.intellij.platform.ai.agent.core.extensions.SnapshotExtensionPointCache -import com.intellij.platform.ai.agent.core.session.AgentSessionProvider -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.extensions.ExtensionPointName -import org.jetbrains.annotations.ApiStatus.Internal - -private class AgentSessionProviderRegistryLog - -private val LOG = logger() - -private val AGENT_SESSION_PROVIDER_EP: ExtensionPointName = - ExtensionPointName("com.intellij.agent.workbench.sessionProvider") - -private data class AgentSessionProviderSnapshot( - @JvmField val providersById: Map, - @JvmField val displayOrderedProviders: List, - @JvmField val providersByIdOrder: List, - @JvmField val sessionSources: List, -) { - companion object { - val EMPTY = AgentSessionProviderSnapshot( - providersById = emptyMap(), - displayOrderedProviders = emptyList(), - providersByIdOrder = emptyList(), - sessionSources = emptyList(), - ) - } -} - -private val PROVIDER_SNAPSHOT_CACHE = SnapshotExtensionPointCache( - log = LOG, - extensionPoint = AGENT_SESSION_PROVIDER_EP, - cacheId = AgentSessionProviderSnapshot::class.java, - emptySnapshot = AgentSessionProviderSnapshot.EMPTY, - unavailableMessage = "Session provider EP is unavailable in this context", - buildSnapshot = ::buildAgentSessionProviderSnapshot, -) - -@Internal -interface AgentSessionProviderRegistry { - fun find(provider: AgentSessionProvider): AgentSessionProviderDescriptor? - - fun allProviders(): List - - fun allProvidersById(): List - - fun sessionSources(): List -} - -private fun buildAgentSessionProviderSnapshot(providers: Iterable): AgentSessionProviderSnapshot { - val providersById = LinkedHashMap() - val uniqueProviders = ArrayList() - for (provider in providers) { - val previous = providersById.putIfAbsent(provider.provider, provider) - if (previous != null && previous !== provider) { - LOG.warn( - "Duplicate session provider for ${provider.provider.value}: keeping ${previous::class.java.name}, ignoring ${provider::class.java.name}", - ) - } - else { - uniqueProviders += provider - } - } - val displayOrderedProviders = uniqueProviders.sortedWith( - compareBy({ it.displayPriority }, { it.provider.value }) - ) - return AgentSessionProviderSnapshot( - providersById = providersById, - displayOrderedProviders = displayOrderedProviders, - providersByIdOrder = uniqueProviders.sortedBy { provider -> provider.provider.value }, - sessionSources = displayOrderedProviders.map { it.sessionSource }, - ) -} - -private class EpBackedAgentSessionProviderRegistry : AgentSessionProviderRegistry { - override fun find(provider: AgentSessionProvider): AgentSessionProviderDescriptor? { - return snapshotOrEmpty().providersById[provider] - } - - override fun allProviders(): List { - return snapshotOrEmpty().displayOrderedProviders - } - - override fun allProvidersById(): List { - return snapshotOrEmpty().providersByIdOrder - } - - override fun sessionSources(): List { - return snapshotOrEmpty().sessionSources - } -} - -private fun snapshotOrEmpty(): AgentSessionProviderSnapshot { - return PROVIDER_SNAPSHOT_CACHE.getSnapshotOrEmpty() -} - -@Internal -class InMemoryAgentSessionProviderRegistry( - providers: Iterable, -) : AgentSessionProviderRegistry { - private val snapshot = buildAgentSessionProviderSnapshot(providers) - - override fun find(provider: AgentSessionProvider): AgentSessionProviderDescriptor? { - return snapshot.providersById[provider] - } - - override fun allProviders(): List { - return snapshot.displayOrderedProviders - } - - override fun allProvidersById(): List { - return snapshot.providersByIdOrder - } - - override fun sessionSources(): List { - return snapshot.sessionSources - } -} - -@Internal -object AgentSessionProviders { - private val epRegistry: AgentSessionProviderRegistry = EpBackedAgentSessionProviderRegistry() - private val registryOverride = OverridableValue { epRegistry } - - fun find(provider: AgentSessionProvider): AgentSessionProviderDescriptor? { - return registryOverride.value().find(provider) - } - - fun sessionSources(): List { - return registryOverride.value().sessionSources() - } - - fun allProviders(): List { - return registryOverride.value().allProviders() - } - - fun allProvidersById(): List { - return registryOverride.value().allProvidersById() - } - - fun withRegistryForTest(registry: AgentSessionProviderRegistry, action: () -> T): T { - return registryOverride.withOverride(registry, action) - } -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionSource.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionSource.kt deleted file mode 100644 index 16513839c96c..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/providers/AgentSessionSource.kt +++ /dev/null @@ -1,702 +0,0 @@ -// 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.providers - -// @spec plugins/air/spec/thread-view/agent-thread-view-structure.spec.md - -import com.intellij.platform.ai.agent.core.AgentThreadActivity -import com.intellij.platform.ai.agent.core.AgentThreadActivityBucket -import com.intellij.platform.ai.agent.core.AgentThreadActivityReport -import com.intellij.platform.ai.agent.core.chromeBucket -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.AgentSessionProvider -import com.intellij.platform.ai.agent.core.session.AgentSessionThread -import com.intellij.platform.ai.agent.core.session.AgentSessionThreadOutline -import com.intellij.openapi.project.Project -import kotlinx.coroutines.flow.Flow -import org.jetbrains.annotations.ApiStatus - -/** - * Lightweight description of a provider thread that can replace a still-pending threadView tab. - * - * Providers return candidates from [AgentSessionRefreshHints] when a newly opened threadView tab does not yet know its concrete provider - * thread id. The refresh coordinator matches candidates by path, provider, recency, and presentation metadata; candidates do not create - * or update persisted rows by themselves. - */ -@ApiStatus.Internal -data class AgentSessionRebindCandidate( - /** Concrete provider thread id that can be rebound to a pending tab. */ - @JvmField val threadId: String, - /** Provider-normalized title for the candidate row or tab. */ - @JvmField val title: String, - /** Provider update timestamp in epoch milliseconds. */ - @JvmField val updatedAt: Long, - /** Best current row activity for the candidate. */ - @JvmField val activity: AgentThreadActivity, -) - -/** - * Describes how much authority an activity update has over the current UI activity. - */ -@ApiStatus.Internal -enum class AgentSessionActivityAuthority { - PROVISIONAL, - DERIVED, - SEMANTIC, - SNAPSHOT, -} - -/** - * Provider activity evidence used when multiple update streams report the same thread. - */ -@ApiStatus.Internal -data class AgentSessionActivityEvidence( - @JvmField val authority: AgentSessionActivityAuthority = AgentSessionActivityAuthority.DERIVED, - @JvmField val canClearAttention: Boolean = true, -) { - companion object { - @JvmField - val PROVISIONAL: AgentSessionActivityEvidence = AgentSessionActivityEvidence( - authority = AgentSessionActivityAuthority.PROVISIONAL, - canClearAttention = false, - ) - - @JvmField - val DERIVED: AgentSessionActivityEvidence = AgentSessionActivityEvidence(AgentSessionActivityAuthority.DERIVED) - - @JvmField - val SEMANTIC: AgentSessionActivityEvidence = AgentSessionActivityEvidence(AgentSessionActivityAuthority.SEMANTIC) - - @JvmField - val SNAPSHOT: AgentSessionActivityEvidence = AgentSessionActivityEvidence(AgentSessionActivityAuthority.SNAPSHOT) - } -} - -/** - * Activity-only update for a known provider thread. - * - * This compact form is used by update events and refresh hints when a provider can report status without reloading the full thread row. - */ -@ApiStatus.Internal -data class AgentSessionThreadActivityUpdate( - /** Row/chrome activity to apply to the thread. */ - @JvmField val activityReport: AgentThreadActivityReport, - /** Whether [activityReport] should also replace the chrome/summary activity. */ - @JvmField val updatesChromeActivity: Boolean = true, - /** Provider update timestamp in epoch milliseconds, or `null` when the update is not timestamped. */ - @JvmField val updatedAt: Long? = null, - /** Authority metadata for merging this activity with existing thread state. */ - @JvmField val evidence: AgentSessionActivityEvidence = AgentSessionActivityEvidence.DERIVED, -) - -/** - * Presentation update for a known provider thread. - * - * A presentation update may carry title, activity, timestamp, or any combination of them. Consumers merge these updates with existing - * rows and ignore stale timestamped updates. - */ -@ApiStatus.Internal -data class AgentSessionThreadPresentationUpdate( - /** Replacement title, or `null` to keep the current title. */ - @JvmField val title: String? = null, - /** Replacement activity, or `null` to keep the current activity. */ - @JvmField val activityReport: AgentThreadActivityReport? = null, - /** Whether [activityReport] should also replace the chrome/summary activity. */ - @JvmField val updatesChromeActivity: Boolean = true, - /** Provider update timestamp in epoch milliseconds, or `null` when the update is not timestamped. */ - @JvmField val updatedAt: Long? = null, - /** Authority metadata for merging this activity with existing thread state. */ - @JvmField val evidence: AgentSessionActivityEvidence = AgentSessionActivityEvidence.DERIVED, -) - -/** - * Provider-specific hints fetched around a refresh. - * - * Hints are intentionally non-authoritative: they may rebind pending tabs and patch presentation for already loaded rows, but they do not - * add persisted rows or evict missing rows. Providers that can only list threads should skip [AgentSessionRefreshHintsSource]. - */ -@ApiStatus.Internal -data class AgentSessionRefreshHints( - /** Candidate concrete threads for pending editor tabs that do not yet have a provider thread id. */ - @JvmField val rebindCandidates: List = emptyList(), - /** Activity-only patches keyed by concrete thread id or sub-agent id. */ - @JvmField val activityUpdatesByThreadId: Map = emptyMap(), - /** Title/activity/timestamp patches keyed by concrete thread id or sub-agent id. */ - @JvmField val presentationUpdatesByThreadId: Map = emptyMap(), -) - -/** - * Result of creating a new provider thread from a thread-outline item. - * - * [thread] describes the newly created thread. [launchSpecOverride] is used when the new thread must be opened with a provider-specific - * terminal command that differs from the normal resume command. - */ -@ApiStatus.Internal -data class AgentSessionOutlineForkResult( - @JvmField val thread: AgentSessionThread, - @JvmField val launchSpecOverride: AgentSessionTerminalLaunchSpec? = null, -) - -/** Timestamp marker used when a refresh seed has no known row timestamp. */ -@ApiStatus.Internal -const val UNKNOWN_AGENT_SESSION_REFRESH_THREAD_UPDATED_AT: Long = -1L - -/** - * Known thread identity passed to providers when fetching refresh hints. - * - * [updatedAt] is the row timestamp currently held by Agent Workbench, or [UNKNOWN_AGENT_SESSION_REFRESH_THREAD_UPDATED_AT] when unknown. - * [forceRefresh] is true when the triggering event explicitly named this thread and stale cached hints should not be reused. - */ -@ApiStatus.Internal -data class AgentSessionRefreshThreadSeed( - @JvmField val threadId: String, - @JvmField val updatedAt: Long = UNKNOWN_AGENT_SESSION_REFRESH_THREAD_UPDATED_AT, - @JvmField val forceRefresh: Boolean = false, -) - -/** Converts thread ids to refresh seeds with unknown timestamps. */ -@ApiStatus.Internal -fun Collection.toAgentSessionRefreshThreadSeeds(): Set { - return asSequence() - .map { threadId -> AgentSessionRefreshThreadSeed(threadId = threadId) } - .toCollection(LinkedHashSet()) -} - -/** Describes how a provider update event should be consumed by the refresh scheduler. */ -@ApiStatus.Internal -enum class AgentSessionSourceUpdate { - /** Thread rows may have been added, removed, or changed; an authoritative provider refresh may be required. */ - THREADS_CHANGED, - - /** Only auxiliary hints or presentation may have changed; consumers should avoid a full row refresh when possible. */ - HINTS_CHANGED, -} - -/** - * Event emitted by [AgentSessionUpdateSource] or [AgentSessionActiveThreadUpdateSource]. - * - * Scope fields narrow the affected paths and thread ids. A `null` scope means all loaded paths or all relevant threads may be affected; - * an empty scope is normalized away by consumers. Project-file evidence lets the scheduler trigger VFS refreshes only when a provider has - * observed possible changes to project files. - */ -@ApiStatus.Internal -class AgentSessionSourceUpdateEvent private constructor( - @JvmField val type: AgentSessionSourceUpdate, - @JvmField val scopedPaths: Set? = null, - @JvmField val threadIds: Set? = null, - @JvmField val activityUpdatesByThreadId: Map = emptyMap(), - @JvmField val presentationUpdatesByThreadId: Map = emptyMap(), - @JvmField val mayHaveChangedProjectFiles: Boolean = false, - @JvmField val changedProjectFilePaths: Set? = null, -) { - companion object { - /** - * Creates an event indicating that provider thread rows may have changed. - * - * Use this for additions, removals, archive transitions, title changes that need authoritative reload, or status changes that cannot - * be expressed as hints. [scopedPaths] and [threadIds] should be as narrow as the provider can report reliably. - */ - fun threadsChanged( - scopedPaths: Set? = null, - threadIds: Set? = null, - activityUpdatesByThreadId: Map = emptyMap(), - presentationUpdatesByThreadId: Map = emptyMap(), - mayHaveChangedProjectFiles: Boolean = false, - changedProjectFilePaths: Set? = null, - ): AgentSessionSourceUpdateEvent { - return AgentSessionSourceUpdateEvent( - type = AgentSessionSourceUpdate.THREADS_CHANGED, - scopedPaths = scopedPaths, - threadIds = threadIds, - activityUpdatesByThreadId = activityUpdatesByThreadId, - presentationUpdatesByThreadId = presentationUpdatesByThreadId, - mayHaveChangedProjectFiles = mayHaveChangedProjectFiles, - changedProjectFilePaths = changedProjectFilePaths, - ) - } - - /** - * Creates an event indicating that provider hints or presentation changed without requiring a full row snapshot. - * - * Consumers may still load a missing thread snapshot when [threadIds] names a row that is not currently present. - */ - fun hintsChanged( - scopedPaths: Set? = null, - threadIds: Set? = null, - activityUpdatesByThreadId: Map = emptyMap(), - presentationUpdatesByThreadId: Map = emptyMap(), - mayHaveChangedProjectFiles: Boolean = false, - changedProjectFilePaths: Set? = null, - ): AgentSessionSourceUpdateEvent { - return AgentSessionSourceUpdateEvent( - type = AgentSessionSourceUpdate.HINTS_CHANGED, - scopedPaths = scopedPaths, - threadIds = threadIds, - activityUpdatesByThreadId = activityUpdatesByThreadId, - presentationUpdatesByThreadId = presentationUpdatesByThreadId, - mayHaveChangedProjectFiles = mayHaveChangedProjectFiles, - changedProjectFilePaths = changedProjectFilePaths, - ) - } - - /** Creates a hint event carrying activity updates for known thread ids. */ - fun activityChanged( - scopedPaths: Set? = null, - threadIds: Set? = null, - activityUpdatesByThreadId: Map, - ): AgentSessionSourceUpdateEvent { - return hintsChanged( - scopedPaths = scopedPaths, - threadIds = threadIds, - activityUpdatesByThreadId = activityUpdatesByThreadId, - ) - } - - /** Creates a hint event carrying title/activity/timestamp presentation updates for known thread ids. */ - fun presentationChanged( - scopedPaths: Set? = null, - threadIds: Set? = null, - presentationUpdatesByThreadId: Map, - ): AgentSessionSourceUpdateEvent { - return hintsChanged( - scopedPaths = scopedPaths, - threadIds = threadIds, - presentationUpdatesByThreadId = presentationUpdatesByThreadId, - ) - } - - /** Creates a row-discovery event, optionally scoped to paths and project-file evidence. */ - fun discoveryChanged( - scopedPaths: Set? = null, - mayHaveChangedProjectFiles: Boolean = false, - changedProjectFilePaths: Set? = null, - ): AgentSessionSourceUpdateEvent { - return threadsChanged( - scopedPaths = scopedPaths, - mayHaveChangedProjectFiles = mayHaveChangedProjectFiles, - changedProjectFilePaths = changedProjectFilePaths, - ) - } - - /** - * Creates a discovery event that also reports possible project-file changes. - * - * [changedProjectFilePaths] should contain absolute project-file paths when the provider can report exact files; `null` means the - * provider only knows that some project file under the scoped paths may have changed. - */ - fun projectFilesChanged( - scopedPaths: Set? = null, - changedProjectFilePaths: Set? = null, - ): AgentSessionSourceUpdateEvent { - return discoveryChanged( - scopedPaths = scopedPaths, - mayHaveChangedProjectFiles = true, - changedProjectFilePaths = changedProjectFilePaths, - ) - } - - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is AgentSessionSourceUpdateEvent) return false - - return type == other.type && - scopedPaths == other.scopedPaths && - threadIds == other.threadIds && - activityUpdatesByThreadId == other.activityUpdatesByThreadId && - presentationUpdatesByThreadId == other.presentationUpdatesByThreadId && - mayHaveChangedProjectFiles == other.mayHaveChangedProjectFiles && - changedProjectFilePaths == other.changedProjectFilePaths - } - - override fun hashCode(): Int { - var result = type.hashCode() - result = 31 * result + scopedPaths.hashCode() - result = 31 * result + threadIds.hashCode() - result = 31 * result + activityUpdatesByThreadId.hashCode() - result = 31 * result + presentationUpdatesByThreadId.hashCode() - result = 31 * result + mayHaveChangedProjectFiles.hashCode() - result = 31 * result + changedProjectFilePaths.hashCode() - return result - } - - override fun toString(): String { - return "AgentSessionSourceUpdateEvent(" + - "type=$type, " + - "scopedPaths=$scopedPaths, " + - "threadIds=$threadIds, " + - "activityUpdatesByThreadId=$activityUpdatesByThreadId, " + - "presentationUpdatesByThreadId=$presentationUpdatesByThreadId, " + - "mayHaveChangedProjectFiles=$mayHaveChangedProjectFiles, " + - "changedProjectFilePaths=$changedProjectFilePaths)" - } -} - -/** Converts an activity-only update to a presentation update. */ -@ApiStatus.Internal -fun AgentSessionThreadActivityUpdate.toPresentationUpdate(): AgentSessionThreadPresentationUpdate { - return AgentSessionThreadPresentationUpdate( - activityReport = activityReport, - updatesChromeActivity = updatesChromeActivity, - updatedAt = updatedAt, - evidence = evidence, - ) -} - -/** Merges an incoming activity report with an existing report according to the incoming evidence authority. */ -@ApiStatus.Internal -fun mergeAgentThreadActivityReport( - existing: AgentThreadActivityReport, - incoming: AgentThreadActivityReport, - incomingUpdatesChromeActivity: Boolean, - incomingEvidence: AgentSessionActivityEvidence, -): AgentThreadActivityReport { - val chromeActivity = when { - !incomingUpdatesChromeActivity -> existing.chromeActivity - shouldPreserveAttentionChrome(existing = existing, incoming = incoming, incomingEvidence = incomingEvidence) -> existing.chromeActivity - else -> incoming.chromeActivity - } - return AgentThreadActivityReport( - rowActivity = incoming.rowActivity, - chromeActivity = chromeActivity, - ) -} - -/** - * Merges two presentation updates for the same thread id. - * - * Stale timestamped updates are ignored. Non-null incoming fields replace existing fields, while chrome-activity behavior is merged so a - * later row-only update does not accidentally erase a chrome activity supplied by an earlier update. - */ -@ApiStatus.Internal -fun mergeAgentSessionThreadPresentationUpdates( - existing: AgentSessionThreadPresentationUpdate, - incoming: AgentSessionThreadPresentationUpdate, -): AgentSessionThreadPresentationUpdate { - val existingUpdatedAt = existing.updatedAt - val incomingUpdatedAt = incoming.updatedAt - if (existingUpdatedAt != null && incomingUpdatedAt != null && incomingUpdatedAt < existingUpdatedAt) { - return existing - } - val updatedAt = when { - existingUpdatedAt == null -> incomingUpdatedAt - incomingUpdatedAt == null -> existingUpdatedAt - else -> maxOf(existingUpdatedAt, incomingUpdatedAt) - } - val updatesChromeActivity = incoming.updatesChromeActivity || existing.updatesChromeActivity - val activityReport = when { - incoming.activityReport == null -> existing.activityReport - existing.activityReport == null -> incoming.activityReport - else -> mergeAgentThreadActivityReport( - existing = existing.activityReport, - incoming = incoming.activityReport, - incomingUpdatesChromeActivity = incoming.updatesChromeActivity, - incomingEvidence = incoming.evidence, - ) - } - return AgentSessionThreadPresentationUpdate( - title = incoming.title ?: existing.title, - activityReport = activityReport, - updatesChromeActivity = updatesChromeActivity, - updatedAt = updatedAt, - evidence = mergeAgentSessionActivityEvidence(existing = existing.evidence, incoming = incoming.evidence), - ) -} - -private fun shouldPreserveAttentionChrome( - existing: AgentThreadActivityReport, - incoming: AgentThreadActivityReport, - incomingEvidence: AgentSessionActivityEvidence, -): Boolean { - return !incomingEvidence.canClearAttention && - existing.chromeBucket() == AgentThreadActivityBucket.ATTENTION && - incoming.chromeBucket() != AgentThreadActivityBucket.ATTENTION -} - -/** Keeps the strongest activity evidence seen while coalescing updates for a thread. */ -@ApiStatus.Internal -fun mergeAgentSessionActivityEvidence( - existing: AgentSessionActivityEvidence, - incoming: AgentSessionActivityEvidence, -): AgentSessionActivityEvidence { - return if (incoming.authority.ordinal >= existing.authority.ordinal) incoming else existing -} - -/** Request passed to [AgentSessionRefreshSource] for provider-scoped refreshes. */ -@ApiStatus.Internal -data class AgentSessionSourceRefreshRequest( - /** Normalized Agent Workbench project/worktree paths that need provider rows. */ - @JvmField val paths: List, - /** Concrete thread ids named by the triggering update, or empty for a path-level refresh. */ - @JvmField val threadIds: Set = emptySet(), - /** Source update that triggered this refresh. */ - @JvmField val updateEvent: AgentSessionSourceUpdateEvent, - /** Optional provider source paths keyed by Agent Workbench identity path. */ - @JvmField val projectDirectoriesByPath: Map = emptyMap(), -) { - /** True when [threadIds] requests a partial thread-level refresh. */ - val isThreadScoped: Boolean - get() = threadIds.isNotEmpty() - - fun projectDirectoryFor(path: String): String? { - return projectDirectoriesByPath[normalizeAgentWorkbenchPath(path)] - ?.takeIf { it.isNotBlank() } - ?.let(::normalizeAgentWorkbenchPath) - } - - fun sourcePathFor(path: String): String { - return projectDirectoryFor(path) ?: path - } - - fun sourcePaths(): List { - return paths.map { path -> sourcePathFor(path) } - } -} - -/** - * Result returned by [AgentSessionRefreshSource]. - * - * [completeThreadsByPath] is authoritative for a path and replaces all active rows for the provider there. [partialThreadsByPath] updates - * only returned thread ids and must not evict other rows. [removedThreadIdsByPath] removes rows during a partial refresh. [failuresByPath] - * reports path-local failures while allowing other paths in the same request to succeed. - */ -@ApiStatus.Internal -data class AgentSessionSourceRefreshResult( - @JvmField val completeThreadsByPath: Map> = emptyMap(), - @JvmField val partialThreadsByPath: Map> = emptyMap(), - @JvmField val removedThreadIdsByPath: Map> = emptyMap(), - @JvmField val failuresByPath: Map = emptyMap(), -) - -/** Returns true when an update event is not scoped to paths or thread ids. */ -@ApiStatus.Internal -fun AgentSessionSourceUpdateEvent.isUnscoped(): Boolean { - return scopedPaths == null && threadIds == null -} - -/** Formats update scope for debug logging without exposing concrete paths or thread ids. */ -@ApiStatus.Internal -fun AgentSessionSourceUpdateEvent.describeScope(): String { - val scopedPaths = scopedPaths - val threadIds = threadIds - val scope = when { - scopedPaths == null && threadIds == null -> "scope=all" - scopedPaths != null && threadIds != null -> "scope=paths:${scopedPaths.size},threadIds:${threadIds.size}" - scopedPaths != null -> "scope=paths:${scopedPaths.size}" - else -> "scope=threadIds:${threadIds?.size ?: 0}" - } - return changedProjectFilePaths?.let { paths -> "$scope,changedProjectFiles:${paths.size}" } ?: scope -} - -/** - * Required provider source contract for Agent Workbench session discovery. - * - * Provider implementations normally register one provider descriptor and expose one stable source from it. This source owns active thread - * listing and provider-local session discovery state. Optional behavior belongs in the focused capability interfaces below, implemented - * on the same source when supported; do not add default no-op methods here. - * - * The path accepted by methods in this file is the normalized Agent Workbench project or worktree path. [Project] is supplied only when the - * path belongs to a currently open IDE project. - */ -@ApiStatus.Internal -interface AgentSessionSource { - /** Stable provider id for every thread returned by this source. */ - val provider: AgentSessionProvider - - /** - * Whether normal active-thread loading can report the exact total number of available rows for this provider. - * - * Return `false` when discovery is intentionally partial or backend-limited. The tool window then treats visible counts as unknown - * instead of implying that no more provider rows exist. - */ - val canReportExactThreadCount: Boolean - get() = true - - /** - * Loads active, non-archived threads for [path]. - * - * Return rows with [AgentSessionThread.provider] equal to [provider], stable concrete ids, provider-normalized titles, and epoch-millis - * `updatedAt` values. [openProject] is non-null only when the path belongs to a currently open IDE project; implementations may ignore - * it when provider storage is project-path based. Do not return archived rows here; implement [AgentSessionArchivedSource] instead. - */ - suspend fun listThreads(path: String, openProject: Project?): List -} - -/** Capability for providers that can batch-load active threads for several paths. */ -@ApiStatus.Internal -interface AgentSessionPrefetchSource : AgentSessionSource { - /** - * Prefetches active threads for [paths] in one backend call. - * - * Return only paths that were actually loaded. Missing paths fall back to [AgentSessionSource.listThreads]. Returned lists are complete - * active snapshots for their path and must follow the same row contract as [AgentSessionSource.listThreads]. - */ - suspend fun prefetchThreads(paths: List): Map> -} - -/** Capability for providers that expose archived threads separately from active threads. */ -@ApiStatus.Internal -interface AgentSessionArchivedSource : AgentSessionSource { - /** - * Loads archived threads for [path]. - * - * Return only archived rows. [openProject] has the same meaning as in [AgentSessionSource.listThreads]. Providers that cannot list - * archived rows cheaply or accurately should not implement this capability. - */ - suspend fun listArchivedThreads(path: String, openProject: Project?): List -} - -/** Capability for providers that emit background updates for loaded session rows or hints. */ -@ApiStatus.Internal -interface AgentSessionUpdateSource : AgentSessionSource { - /** - * Stream of provider update events. - * - * Implementations should emit only meaningful changes after filtering raw filesystem/backend noise. Events may be scoped to paths, - * thread ids, or both; precise scopes reduce refresh work and make active threadView tabs update faster. - */ - val updateEvents: Flow -} - -/** Capability for providers that can watch one active concrete thread with provider-specific filtering. */ -@ApiStatus.Internal -interface AgentSessionActiveThreadUpdateSource : AgentSessionSource { - /** - * Returns update events for an actively running [threadId] under [path]. - * - * This stream is used by active threadView tabs and outlines. Implementations should parse raw notifications and emit only meaningful source - * updates such as activity changes or project-file evidence; unchanged persistence writes should not become refresh signals. - */ - fun activeThreadUpdateEvents(path: String, threadId: String): Flow -} - -/** Capability for providers that can refresh rows more precisely than path-level listing. */ -@ApiStatus.Internal -interface AgentSessionRefreshSource : AgentSessionSource { - /** - * Refreshes provider rows for [AgentSessionSourceRefreshRequest.paths]. - * - * Return complete path snapshots when the provider performed an authoritative path listing. Return partial rows and removals for - * thread-scoped updates. Report path-local failures in [AgentSessionSourceRefreshResult.failuresByPath] instead of failing the whole - * request when other paths can still succeed. - */ - suspend fun refreshThreads(request: AgentSessionSourceRefreshRequest): AgentSessionSourceRefreshResult -} - -/** Capability for providers that can cheaply fetch non-authoritative refresh hints. */ -@ApiStatus.Internal -interface AgentSessionRefreshHintsSource : AgentSessionSource { - /** - * Loads provider-specific hints for [paths]. - * - * [refreshThreadSeedsByPath] contains loaded or otherwise interesting thread ids with their current timestamps. Returned hints must not - * add or remove persisted rows directly; they are consumed for pending-tab rebinding and presentation/status projection. - */ - suspend fun prefetchRefreshHints( - paths: List, - refreshThreadSeedsByPath: Map>, - ): Map -} - -/** Capability for providers that hydrate cost after visible rows are known. */ -@ApiStatus.Internal -interface AgentSessionCostSource : AgentSessionSource { - /** - * Loads cost for the requested [threads] under [path]. - * - * Implementations should key the result by concrete thread id and avoid recomputing work already represented by an unchanged - * `updatedAt`. A missing map entry or `null` value means the cost is unavailable for that thread. - */ - suspend fun loadThreadCosts( - path: String, - threads: List, - ): Map -} - -/** Capability for providers that expose a read-only outline of persisted thread history. */ -@ApiStatus.Internal -interface AgentSessionThreadOutlineSource : AgentSessionSource { - /** - * Loads a read-only, role-aware outline for a persisted thread. - * - * Implementations should read provider history without restoring terminals, driving TUIs, or mutating provider state. Return `null` - * when an outline is unavailable. Return an [AgentSessionThreadOutline] with an empty item list when the provider supports outlines but - * has no visible entries. Item ids must be stable provider anchors when navigation or fork capabilities use them. - */ - suspend fun loadThreadOutline( - path: String, - threadId: String, - subAgentId: String? = null, - ): AgentSessionThreadOutline? -} - -/** Capability for providers that can navigate a live provider view to an outline item. */ -@ApiStatus.Internal -interface AgentSessionThreadOutlineNavigationSource : AgentSessionThreadOutlineSource { - /** - * Returns whether [itemId] has a stable live provider anchor that can be navigated to in the current thread context. - * - * This is not a fallback hook for launching a provider, scraping terminal output, or replaying a TUI selection. Providers that only - * display persisted history should not implement this capability. - */ - fun canNavigateThreadOutlineItem( - path: String, - threadId: String, - itemId: String, - subAgentId: String? = null, - tabKey: String? = null, - ): Boolean - - /** Navigates a live provider view to [itemId] when [canNavigateThreadOutlineItem] reports support. */ - suspend fun navigateThreadOutlineItem( - path: String, - threadId: String, - itemId: String, - subAgentId: String? = null, - tabKey: String? = null, - ): Boolean -} - -/** Capability for providers that can create a new thread from an outline item. */ -@ApiStatus.Internal -interface AgentSessionThreadOutlineForkSource : AgentSessionThreadOutlineSource { - /** - * Runtime visibility and executable gate for forking from [itemId]. - * - * Return `true` only when the current provider state can create the fork now. UI surfaces use this single gate for both visibility and - * enablement, so do not return `true` for conceptual support that may still fail because a live bridge is disconnected. - */ - fun canForkThreadFromOutlineItem( - path: String, - threadId: String, - itemId: String, - subAgentId: String? = null, - tabKey: String? = null, - ): Boolean - - /** - * Creates a provider-specific fork from [itemId]. - * - * Implementations must not mutate the source thread. The returned [AgentSessionOutlineForkResult.thread] describes the new thread that - * callers can open and refresh after the fork completes. Return `null` if the fork is no longer possible. - */ - suspend fun forkThreadFromOutlineItem( - project: Project, - path: String, - threadId: String, - itemId: String, - subAgentId: String? = null, - tabKey: String? = null, - ): AgentSessionOutlineForkResult? -} - -/** Capability for providers that track read/unread state relative to open threadView tabs. */ -@ApiStatus.Internal -interface AgentSessionReadStateSource : AgentSessionSource { - /** Records that [threadId] is the active thread for this source, or clears it with `null`. */ - fun setActiveThreadId(threadId: String?) - - /** Marks [threadId] as read through [updatedAt] and emits any provider-local hint update needed to clear unread state. */ - fun markThreadAsRead(threadId: String, updatedAt: Long) -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/providers/BaseAgentSessionSource.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/providers/BaseAgentSessionSource.kt deleted file mode 100644 index 302fb0bec74d..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/providers/BaseAgentSessionSource.kt +++ /dev/null @@ -1,146 +0,0 @@ -// 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.providers - -import com.intellij.platform.ai.agent.core.AgentThreadActivity -import com.intellij.platform.ai.agent.core.session.AgentSessionProvider -import com.intellij.platform.ai.agent.core.session.AgentSessionThread -import com.intellij.openapi.project.Project -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import org.jetbrains.annotations.ApiStatus -import java.util.concurrent.ConcurrentHashMap - -/** - * Default base class for provider sources that store read/unread state in Agent Workbench. - * - * Subclasses implement [loadThreads] for active thread discovery and may add focused capabilities such as [AgentSessionArchivedSource], - * [AgentSessionRefreshSource], or [AgentSessionThreadOutlineSource] on the same concrete source. The base class keeps - * [AgentSessionSource.listThreads] final so shared services always pass through one read-state-aware entry point. - */ -@ApiStatus.Internal -abstract class BaseAgentSessionSource( - final override val provider: AgentSessionProvider, - final override val canReportExactThreadCount: Boolean = true, -) : AgentSessionSource, AgentSessionReadStateSource { - /** - * Tracks the last-seen `updatedAt` for threads the user has opened. - * Absent key = never opened -> READY (not UNREAD). - * Present key = opened at least once; if `thread.updatedAt > storedValue` -> UNREAD. - */ - protected val readTracker: ConcurrentHashMap = ConcurrentHashMap() - - private val readStateUpdates = MutableSharedFlow(extraBufferCapacity = 1) - - protected val readStateUpdateEvents: Flow = readStateUpdates - - @Volatile - protected var activeThreadId: String? = null - private set - - final override fun setActiveThreadId(threadId: String?) { - activeThreadId = threadId - } - - final override fun markThreadAsRead(threadId: String, updatedAt: Long) { - var advanced = false - readTracker.compute(threadId) { _, current -> - if (current == null || updatedAt > current) { - advanced = true - updatedAt - } - else { - current - } - } - if (advanced) { - readStateUpdates.tryEmit( - AgentSessionSourceUpdateEvent.hintsChanged( - threadIds = setOf(threadId), - ) - ) - } - } - - /** - * Records the currently active thread as read when it is present in [threads]. - * - * This helper is intended for provider parsers that already have a fresh active-thread snapshot. It stops at the first matching id and - * merges the row `updatedAt` into [readTracker] only when [shouldRemember] accepts the row. - */ - protected inline fun rememberActiveThreadRead( - threads: Iterable, - id: (T) -> String, - updatedAt: (T) -> Long, - shouldRemember: (T) -> Boolean = { true }, - ) { - val currentActiveId = activeThreadId ?: return - for (thread in threads) { - if (id(thread) == currentActiveId) { - if (shouldRemember(thread)) { - readTracker.merge(currentActiveId, updatedAt(thread), ::maxOf) - } - return - } - } - } - - final override suspend fun listThreads(path: String, openProject: Project?): List { - return loadThreads(path = path, openProject = openProject) - } - - /** - * Loads active, non-archived threads for [path]. - * - * Implementations must follow the same row contract as [AgentSessionSource.listThreads]. [openProject] is non-null only for an open IDE - * project whose normalized path is [path]; closed project and worktree discovery must not require it. - */ - protected abstract suspend fun loadThreads(path: String, openProject: Project?): List - - /** - * Fallback [AgentSessionRefreshSource.refreshThreads] implementation based on normal active-thread listing. - * - * Providers with only path-level discovery can delegate to this method from their refresh capability. It prefetches first when the same - * source also implements [AgentSessionPrefetchSource], then returns complete path snapshots and path-local failures. - */ - protected suspend fun refreshThreadsByListing(request: AgentSessionSourceRefreshRequest): AgentSessionSourceRefreshResult { - if (request.paths.isEmpty()) { - return AgentSessionSourceRefreshResult() - } - - val prefetched = try { - (this as? AgentSessionPrefetchSource)?.prefetchThreads(request.sourcePaths()).orEmpty() - } - catch (e: Throwable) { - if (e is CancellationException) throw e - emptyMap() - } - val completeThreadsByPath = LinkedHashMap>(request.paths.size) - val failuresByPath = LinkedHashMap() - for (path in request.paths) { - val sourcePath = request.sourcePathFor(path) - val prefetchedThreads = prefetched[sourcePath] - if (prefetchedThreads != null) { - completeThreadsByPath[path] = prefetchedThreads - continue - } - try { - completeThreadsByPath[path] = listThreads(path = sourcePath, openProject = null) - } - catch (e: Throwable) { - if (e is CancellationException) throw e - failuresByPath[path] = e - } - } - return AgentSessionSourceRefreshResult( - completeThreadsByPath = completeThreadsByPath, - failuresByPath = failuresByPath, - ) - } -} - -@ApiStatus.Internal -fun resolveReadTrackedActivity(readTracker: Map, threadId: String, updatedAt: Long): AgentThreadActivity { - val lastSeenAt = readTracker[threadId] ?: return AgentThreadActivity.READY - return if (updatedAt > lastSeenAt) AgentThreadActivity.UNREAD else AgentThreadActivity.READY -} diff --git a/plugins/agent-workbench/lib-agent/sessions-core/src/util/JbCentralCliSupport.kt b/plugins/agent-workbench/lib-agent/sessions-core/src/util/JbCentralCliSupport.kt deleted file mode 100644 index ffa570d86253..000000000000 --- a/plugins/agent-workbench/lib-agent/sessions-core/src/util/JbCentralCliSupport.kt +++ /dev/null @@ -1,82 +0,0 @@ -// 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.util - -import com.intellij.execution.configurations.PathEnvironmentVariableUtil -import com.intellij.openapi.util.SystemInfoRt -import com.intellij.util.SystemProperties -import org.jetbrains.annotations.TestOnly -import java.nio.file.Files -import java.nio.file.Path - -const val AGENT_WORKBENCH_JBCENTRAL_PATH_PROPERTY: String = "agent.workbench.sessions.jbcentral.path" - -object JbCentralCliSupport { - private const val JBCENTRAL_COMMAND: String = "jbcentral" - private const val LEGACY_COMMAND: String = "wire" - - fun findExecutable(): String? { - val configuredPath = System.getProperty(AGENT_WORKBENCH_JBCENTRAL_PATH_PROPERTY)?.trim().orEmpty() - if (configuredPath.isNotEmpty()) { - return resolveConfiguredExecutable(configuredPath) - } - - findExecutableOnPath()?.let { return it } - - val homeDir = SystemProperties.getUserHome().takeIf(String::isNotBlank) ?: return null - for (candidate in fallbackCandidates(homeDir)) { - if (Files.isRegularFile(candidate)) { - return candidate.toAbsolutePath().toString() - } - } - return null - } - - fun isAvailable(): Boolean = findExecutable() != null - - private fun resolveConfiguredExecutable(configuredPath: String): String? { - val path = Path.of(configuredPath).toAbsolutePath() - return path.takeIf(Files::isRegularFile)?.toString() - } - - private fun findExecutableOnPath(): String? { - JbCentralCliSupportTestHook.pathLookupOverride()?.let { override -> - return override() - } - for (command in commandCandidates()) { - PathEnvironmentVariableUtil.findExecutableInPathOnAnyOS(command)?.absolutePath?.let { return it } - } - return null - } - - private fun fallbackCandidates(homeDir: String): List { - val installDir = Path.of(homeDir, ".local", "bin") - return fallbackExecutableNames().map { executableName -> - installDir.resolve(executableName) - } - } - - private fun commandCandidates(): List = listOf(JBCENTRAL_COMMAND, LEGACY_COMMAND) - - private fun fallbackExecutableNames(): List { - return if (SystemInfoRt.isWindows) { - listOf("$JBCENTRAL_COMMAND.exe", "$LEGACY_COMMAND.cmd", "$LEGACY_COMMAND.exe") - } - else { - listOf(JBCENTRAL_COMMAND, LEGACY_COMMAND) - } - } -} - -object JbCentralCliSupportTestHook { - @Volatile - private var pathLookupOverride: (() -> String?)? = null - - fun pathLookupOverride(): (() -> String?)? = pathLookupOverride - - @TestOnly - fun replacePathLookupForTest(pathLookup: (() -> String?)?): (() -> String?)? { - val previous = pathLookupOverride - pathLookupOverride = pathLookup - return previous - } -} diff --git a/plugins/agent-workbench/prompt/core/BUILD.bazel b/plugins/agent-workbench/prompt/core/BUILD.bazel deleted file mode 100644 index 681cda64177d..000000000000 --- a/plugins/agent-workbench/prompt/core/BUILD.bazel +++ /dev/null @@ -1,187 +0,0 @@ -load("@community//build:tests-options.bzl", "jps_test") -load("@rules_jvm//:jvm.bzl", "jvm_library") - -### auto-generated section `build intellij.agent.workbench.prompt.core` start - -jvm_library( - name = "core", - srcs = glob( - [ - "src/**/*.kt", - "src/**/*.java", - "src/**/*.form", - ], - allow_empty = True, - ), - module_name = "intellij.agent.workbench.prompt.core", - resource_strip_prefix = "resources", - resources = glob( - ["resources/**/*"], - ), - visibility = ["//visibility:public"], - deps = [ - # do not sort, - "@lib//:kotlin-stdlib", - "//libraries/kotlinx/serialization/core", - "//libraries/hash4j", - "//platform/analysis-api:analysis", - "//platform/util/concurrency", - "//platform/core-api:core", - "//platform/core-ui", - "//platform/platform-api:ide", - "//platform/platform-impl:ide-impl", - "//platform/platform-impl/concurrency", - "//platform/lang-api:lang", - "//platform/lang-impl", - "//platform/projectModel-api:projectModel", - "//platform/util", - "//platform/util:util-ui", - "//platform/editor-ui-api:editor-ui", - "//plugins/agent-workbench/lib-agent/core", - ], -) - -jvm_library( - name = "core_test_lib", - testonly = True, - srcs = glob( - [], - allow_empty = True, - ), - module_name = "intellij.agent.workbench.prompt.core", - visibility = ["//visibility:public"], - runtime_deps = [ - # do not sort, - ":core", - "//libraries/kotlinx/serialization/core:core_test_lib", - "//libraries/hash4j:hash4j_test_lib", - "//platform/analysis-api:analysis_test_lib", - "//platform/util/concurrency:concurrency_test_lib", - "//platform/core-api:core_test_lib", - "//platform/core-ui:core-ui_test_lib", - "//platform/platform-api:ide_test_lib", - "//platform/platform-impl:ide-impl_test_lib", - "//platform/platform-impl/concurrency:concurrency_test_lib", - "//platform/lang-api:lang_test_lib", - "//platform/lang-impl:lang-impl_test_lib", - "//platform/projectModel-api:projectModel_test_lib", - "//platform/util:util_test_lib", - "//platform/util:util-ui_test_lib", - "//platform/editor-ui-api:editor-ui_test_lib", - "//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.prompt.core` end - -### auto-generated section `iml intellij.agent.workbench.prompt.core` start -exports_files( - [ - "intellij.agent.workbench.prompt.core.iml", - ], - visibility = ["//visibility:public"], -) -### auto-generated section `iml intellij.agent.workbench.prompt.core` end - -### auto-generated section `build intellij.agent.workbench.prompt.core.tests` start - -jvm_library( - name = "agent-workbench-prompt-core-tests", - srcs = glob( - [], - allow_empty = True, - ), - module_name = "intellij.agent.workbench.prompt.core.tests", - visibility = ["//visibility:public"], -) - -jvm_library( - name = "agent-workbench-prompt-core-tests_test_lib", - testonly = True, - srcs = glob( - [ - "testSrc/**/*.kt", - "testSrc/**/*.java", - "testSrc/**/*.form", - ], - allow_empty = True, - ), - associates = [ - "//plugins/agent-workbench/prompt/core", - "//plugins/agent-workbench/prompt/core:core_test_lib", - ], - visibility = ["//visibility:public"], - runtime_deps = [ - # do not sort, - ":agent-workbench-prompt-core-tests", - "//platform/platform-impl:ide-impl_test_lib", - "//platform/platform-impl/concurrency:concurrency_test_lib", - ], - deps = [ - # do not sort, - "@lib//:kotlin-stdlib", - "//libraries/kotlinx/serialization/core", - "//libraries/kotlinx/serialization/core:core_test_lib", - "//libraries/hash4j", - "//libraries/hash4j:hash4j_test_lib", - "//platform/analysis-api:analysis", - "//platform/analysis-api:analysis_test_lib", - "//platform/util/concurrency", - "//platform/util/concurrency:concurrency_test_lib", - "//platform/core-api:core", - "//platform/core-api:core_test_lib", - "//platform/core-ui", - "//platform/core-ui:core-ui_test_lib", - "//platform/platform-api:ide", - "//platform/platform-api:ide_test_lib", - "//platform/platform-impl:ide-impl", - "//platform/editor-ui-api:editor-ui", - "//platform/editor-ui-api:editor-ui_test_lib", - "//platform/platform-impl/concurrency", - "//platform/lang-api:lang", - "//platform/lang-api:lang_test_lib", - "//platform/lang-impl", - "//platform/lang-impl:lang-impl_test_lib", - "//platform/projectModel-api:projectModel", - "//platform/projectModel-api:projectModel_test_lib", - "//platform/util", - "//platform/util:util_test_lib", - "//platform/util:util-ui", - "//platform/util:util-ui_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.prompt.core.tests` end - -### auto-generated section `iml intellij.agent.workbench.prompt.core.tests` start -exports_files( - [ - "intellij.agent.workbench.prompt.core.tests.iml", - ], - visibility = ["//visibility:public"], -) -### auto-generated section `iml intellij.agent.workbench.prompt.core.tests` end - -### auto-generated section `test intellij.agent.workbench.prompt.core.tests` start - -jps_test( - name = "agent-workbench-prompt-core-tests_test", - runtime_deps = [":agent-workbench-prompt-core-tests_test_lib"], -) -### auto-generated section `test intellij.agent.workbench.prompt.core.tests` end diff --git a/plugins/agent-workbench/prompt/core/intellij.agent.workbench.prompt.core.iml b/plugins/agent-workbench/prompt/core/intellij.agent.workbench.prompt.core.iml deleted file mode 100644 index e0408834ea27..000000000000 --- a/plugins/agent-workbench/prompt/core/intellij.agent.workbench.prompt.core.iml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - $KOTLIN_BUNDLED$/lib/kotlinx-serialization-compiler-plugin.jar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/plugins/agent-workbench/prompt/core/intellij.agent.workbench.prompt.core.tests.iml b/plugins/agent-workbench/prompt/core/intellij.agent.workbench.prompt.core.tests.iml deleted file mode 100644 index 1d3f4fa62aae..000000000000 --- a/plugins/agent-workbench/prompt/core/intellij.agent.workbench.prompt.core.tests.iml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - $KOTLIN_BUNDLED$/lib/kotlinx-serialization-compiler-plugin.jar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/plugins/agent-workbench/prompt/core/resources/intellij.agent.workbench.prompt.core.xml b/plugins/agent-workbench/prompt/core/resources/intellij.agent.workbench.prompt.core.xml deleted file mode 100644 index 87ce54b29650..000000000000 --- a/plugins/agent-workbench/prompt/core/resources/intellij.agent.workbench.prompt.core.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - messages.AgentPromptSuggestionsBundle - - - - - - - - - - - - - - - - - - diff --git a/plugins/agent-workbench/prompt/core/resources/messages/AgentPromptSuggestionsBundle.properties b/plugins/agent-workbench/prompt/core/resources/messages/AgentPromptSuggestionsBundle.properties deleted file mode 100644 index 6f2d931b75aa..000000000000 --- a/plugins/agent-workbench/prompt/core/resources/messages/AgentPromptSuggestionsBundle.properties +++ /dev/null @@ -1,30 +0,0 @@ -popup.suggestion.tests.fix.label=Fix failures -popup.suggestion.tests.fix.prompt=Make the failing tests pass. First, in thread view, name for each failure whether the bug is in the production code or the test, with a one-sentence justification. Then fix only the side you named. Do not weaken assertions or delete test cases. -popup.suggestion.tests.explain.label=Explain failures -popup.suggestion.tests.explain.prompt=Explain why these tests are failing and which code is responsible. Do not change anything. -popup.suggestion.tests.bisect.label=Bisect -popup.suggestion.tests.bisect.prompt=Find the commit that introduced these failures. Read `git log -p` on the failing test files and the production code paths they exercise; pick suspects from the diffs, not commit messages. List your top 1-3 candidates with the diff hunk that likely caused the regression. Do not claim certainty unless you actually ran the test at that revision. -popup.suggestion.tests.coverage.label=Explain coverage -popup.suggestion.tests.coverage.prompt=Explain what these tests cover, which behaviors they verify, and which behaviors they leave out. -popup.suggestion.tests.review.label=Audit tests -popup.suggestion.tests.review.prompt=Audit these tests for brittle assumptions, weak assertions, hidden dependencies, or suspicious passes. -popup.suggestion.tests.extend.label=Add tests -popup.suggestion.tests.extend.prompt=Add tests to the same test file(s) as the selected tests, covering behaviors the existing tests miss in the same units under test. For each new test, state the coverage gap it closes. -popup.suggestion.vcs.review.label=Review commits -popup.suggestion.vcs.review.prompt=Review the selected commits for risks, regressions, or missing follow-up work. -popup.suggestion.vcs.summary.label=Summarize commits -popup.suggestion.vcs.summary.prompt=Summarize what changed across these commits, grouped by area; call out anything risky or unusual. -popup.suggestion.vcs.followup.label=List follow-ups -popup.suggestion.vcs.followup.prompt=List the follow-up work, TODOs, and loose ends implied by these commits - tests not yet added, references not yet cleaned up, behavior left for later. -popup.suggestion.editor.explain.label=Explain code -popup.suggestion.editor.explain.prompt=Explain the selected code and how its callers use it. -popup.suggestion.editor.refactor.label=Refactor code -popup.suggestion.editor.refactor.prompt=Refactor the selected code for readability. Do not change observable behavior - same return values, same exceptions, same side effects, same threading. If you cannot guarantee equivalence for a transformation, skip it and say why. List each transformation you applied. -popup.suggestion.editor.review.label=Review code -popup.suggestion.editor.review.prompt=Review the selected code for bugs, edge cases, or missing tests. -popup.suggestion.paths.summary.label=Summarize files -popup.suggestion.paths.summary.prompt=Summarize what these files do and how they fit together. -popup.suggestion.paths.impact.label=Map impact -popup.suggestion.paths.impact.prompt=Map dependencies and call relationships across these files; highlight the likely blast-radius areas if any of them change. -popup.suggestion.paths.duplication.label=Find duplication -popup.suggestion.paths.duplication.prompt=Find duplicated logic, overlapping responsibilities, or near-identical structure across these files. diff --git a/plugins/agent-workbench/prompt/core/src/AgentPromptBuiltinContextRenderers.kt b/plugins/agent-workbench/prompt/core/src/AgentPromptBuiltinContextRenderers.kt deleted file mode 100644 index 84339b846178..000000000000 --- a/plugins/agent-workbench/prompt/core/src/AgentPromptBuiltinContextRenderers.kt +++ /dev/null @@ -1,303 +0,0 @@ -// 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.prompt.core - -// @spec plugins/air/spec/prompt-context/prompt-context-contracts.spec.md - -import com.intellij.openapi.util.io.FileUtil -import com.intellij.openapi.util.io.FileUtilRt -import com.intellij.openapi.util.io.OSAgnosticPathUtil -import com.intellij.openapi.util.text.StringUtil -import com.intellij.util.SystemProperties -import java.nio.file.FileSystems -import java.nio.file.InvalidPathException -import java.nio.file.Path -import kotlin.math.max - -private const val CHIP_PREVIEW_MAX_LENGTH = 60 -private const val TREE_SELECTION_ITEM_ID = "tree.selection" - -internal class AgentPromptSnippetContextRendererBridge : AgentPromptContextRendererBridge { - override val rendererId: String - get() = AgentPromptContextRendererIds.SNIPPET - - override fun renderEnvelope(input: AgentPromptEnvelopeRenderInput): String { - val item = input.item - val payload = item.payload.objOrNull() ?: AgentPromptPayloadValue.Obj.EMPTY - val startLine = payload.number("startLine")?.takeIf { it.isNotBlank() } - val endLine = payload.number("endLine")?.takeIf { it.isNotBlank() } - val selection = payload.bool("selection") - val language = normalizeLanguage(payload.string("language")) - - val details = mutableListOf() - if (startLine != null && endLine != null) { - details += "lines=$startLine-$endLine" - } - selection?.let { details += "selection=$it" } - - val descriptor = buildString { - append("snippet") - if (details.isNotEmpty()) { - append(": ") - append(details.joinToString(separator = " ")) - } - append(renderTruncationSuffix(item)) - } - return descriptor + "\n" + appendCodeBlock(language = language, content = item.body) - } - - override fun renderChip(input: AgentPromptChipRenderInput): AgentPromptChipRender { - return AgentPromptChipRender(text = compactSnippetChipTitle(input.item) ?: input.item.title ?: "Snippet") - } - - private fun compactSnippetChipTitle(item: AgentPromptContextItem): String? { - if (item.itemId == AgentPromptContextItemIds.CHANGES_SELECTION) { - return "Changes" - } - - val payload = item.payload.objOrNull() - if (item.itemId == TREE_SELECTION_ITEM_ID) { - payload?.string("treeKind")?.trim()?.takeIf { it.isNotEmpty() }?.let { treeKind -> - return "Selection ($treeKind)" - } - } - - if (payload?.bool("selection") == false) { - val startLine = payload.number("startLine")?.takeIf { it.isNotBlank() } - val endLine = payload.number("endLine")?.takeIf { it.isNotBlank() } - if (startLine != null && endLine != null) { - return "Caret ($startLine-$endLine)" - } - } - - val title = item.title?.trim()?.takeIf { it.isNotEmpty() } ?: return null - return when { - title.startsWith("Caret Context ") -> "Caret " + title.removePrefix("Caret Context ") - title.startsWith("Tree Selection ") -> "Selection " + title.removePrefix("Tree Selection ") - else -> null - } - } -} - -class AgentPromptFileContextRendererBridge : AgentPromptContextRendererBridge { - override val rendererId: String - get() = AgentPromptContextRendererIds.FILE - - override fun renderEnvelope(input: AgentPromptEnvelopeRenderInput): String { - val item = input.item - val payload = item.payload.objOrNull() - val pathText = payload?.string("path") ?: item.body - val absolutePath = absolutizePath(pathText = pathText, projectPath = input.projectPath) - return "file: $absolutePath${renderTruncationSuffix(item)}" - } - - override fun renderChip(input: AgentPromptChipRenderInput): AgentPromptChipRender { - val payload = input.item.payload.objOrNull() - val pathText = payload?.string("path") ?: input.item.body - val shortened = shortenPathForChip(pathText, input.projectBasePath) - val text = if (shortened.isBlank()) composePathChipText(input.item.title, shortened) else composePathChipText(title = null, preview = shortened) - return AgentPromptChipRender(text = text) - } -} - -class AgentPromptSymbolContextRendererBridge : AgentPromptContextRendererBridge { - override val rendererId: String - get() = AgentPromptContextRendererIds.SYMBOL - - override fun renderEnvelope(input: AgentPromptEnvelopeRenderInput): String { - val item = input.item - return "symbol: ${item.body}${renderTruncationSuffix(item)}" - } - - override fun renderChip(input: AgentPromptChipRenderInput): AgentPromptChipRender { - return AgentPromptChipRender(text = composePreviewChipText(input.item.title, input.item.body)) - } -} - -class AgentPromptPathsContextRendererBridge : AgentPromptContextRendererBridge { - override val rendererId: String - get() = AgentPromptContextRendererIds.PATHS - - override fun renderEnvelope(input: AgentPromptEnvelopeRenderInput): String { - val item = input.item - val paths = extractPaths(item) - .map { absolutizePath(pathText = it, projectPath = input.projectPath) } - val truncation = renderTruncationSuffix(item) - return if (paths.size == 1) { - "path: ${paths[0]}$truncation" - } - else { - buildString { - append("paths:") - append(truncation) - if (paths.isNotEmpty()) { - append('\n') - append(paths.joinToString(separator = "\n")) - } - } - } - } - - override fun renderChip(input: AgentPromptChipRenderInput): AgentPromptChipRender { - val first = extractPaths(input.item).firstOrNull().orEmpty() - val preview = shortenPathForChip(first, input.projectBasePath) - val text = if (preview.isBlank()) composePathChipText(input.item.title, preview) else composePathChipText(title = null, preview = preview) - return AgentPromptChipRender(text = text) - } - - private fun extractPaths(item: AgentPromptContextItem): List { - val payload = item.payload.objOrNull() - val entries = payload?.array("entries") - ?.mapNotNull { value -> - val entry = value.objOrNull() ?: return@mapNotNull null - entry.string("path")?.trim()?.takeIf { it.isNotEmpty() } - } - .orEmpty() - if (entries.isNotEmpty()) { - return entries - } - // Body text may have "dir: " / "file: " prefix — strip it - return item.body - .lineSequence() - .map { it.trim() } - .filter { it.isNotEmpty() } - .map { line -> - when { - line.startsWith("file:", ignoreCase = true) -> line.substringAfter(':').trim() - line.startsWith("dir:", ignoreCase = true) -> line.substringAfter(':').trim() - else -> line - } - } - .toList() - } -} - -internal fun renderTruncationSuffix(item: AgentPromptContextItem): String { - val truncation = item.truncation - if (truncation.reason == AgentPromptContextTruncationReason.NONE) { - return "" - } - return " [truncated=${truncation.reason.name.lowercase()} ${truncation.includedChars}/${truncation.originalChars}]" -} - -internal fun appendCodeBlock(language: String?, content: String): String { - val fence = "`".repeat(max(3, maxConsecutiveBackticks(content) + 1)) - return buildString(content.length + 16) { - append(fence) - language?.let { append(it) } - append('\n') - append(content) - append('\n') - append(fence) - } -} - -internal fun maxConsecutiveBackticks(value: String): Int { - var best = 0 - var current = 0 - value.forEach { ch -> - if (ch == '`') { - current += 1 - if (current > best) { - best = current - } - } - else { - current = 0 - } - } - return best -} - -internal fun normalizeLanguage(raw: String?): String? { - val normalized = raw - ?.trim() - ?.lowercase() - ?.replace(' ', '-') - .orEmpty() - if (normalized.isEmpty()) { - return null - } - return if (normalized.all { ch -> ch.isLetterOrDigit() || ch == '-' || ch == '_' || ch == '+' || ch == '.' || ch == '#' }) { - normalized - } - else { - null - } -} - -internal fun absolutizePath(pathText: String, projectPath: String?): String { - val normalizedPath = pathText.trim() - if (normalizedPath.isEmpty()) { - return "[path-unresolved]" - } - val resolved = resolveAbsolutePath(pathText = normalizedPath, projectPath = projectPath) - return resolved ?: "$normalizedPath [path-unresolved]" -} - -private fun resolveAbsolutePath(pathText: String, projectPath: String?): String? { - return try { - val path = Path.of(pathText) - if (path.isAbsolute) { - path.normalize().toString() - } - else { - val normalizedProjectPath = projectPath - ?.trim() - ?.takeIf { it.isNotEmpty() } - ?: return null - Path.of(normalizedProjectPath) - .resolve(path) - .normalize() - .toString() - } - } - catch (_: InvalidPathException) { - null - } -} - -internal fun composePreviewChipText(title: String?, preview: String): String { - val resolvedTitle = title?.takeIf { it.isNotBlank() } ?: "Context" - val trimmedPreview = preview.trim() - if (trimmedPreview.isEmpty()) { - return resolvedTitle - } - return if (trimmedPreview.length <= CHIP_PREVIEW_MAX_LENGTH) trimmedPreview else trimmedPreview.take(CHIP_PREVIEW_MAX_LENGTH) + "\u2026" -} - -internal fun composePathChipText(title: String?, preview: String): String { - val resolvedTitle = title?.takeIf { it.isNotBlank() } - val trimmedPreview = preview.trim() - if (trimmedPreview.isEmpty()) { - return resolvedTitle ?: "Context" - } - val shortPreview = StringUtil.shortenPathWithEllipsis(trimmedPreview, CHIP_PREVIEW_MAX_LENGTH, true) - return if (resolvedTitle == null) shortPreview else "$resolvedTitle: $shortPreview" -} - -internal fun shortenPathForChip(value: String, projectBasePath: String?): String { - if (!OSAgnosticPathUtil.isAbsolute(value)) { - return value - } - - val path = FileUtilRt.toSystemDependentName(value) - val projectPath = projectBasePath - ?.takeIf { it.isNotBlank() } - ?.let(FileUtilRt::toSystemDependentName) - if (!projectPath.isNullOrBlank() && FileUtil.isAncestor(projectPath, path, false)) { - val relative = FileUtilRt.getRelativePath(projectPath, path, FileSystems.getDefault().separator[0]) - return if (relative.isNullOrEmpty()) "." else relative - } - - val userHome = FileUtilRt.toSystemDependentName(SystemProperties.getUserHome()) - if (FileUtil.isAncestor(userHome, path, false)) { - return if (FileUtil.pathsEqual(userHome, path)) { - "~" - } - else { - FileUtil.getLocationRelativeToUserHome(path, false) - } - } - - return path -} diff --git a/plugins/agent-workbench/prompt/core/src/AgentPromptContainerLauncher.kt b/plugins/agent-workbench/prompt/core/src/AgentPromptContainerLauncher.kt deleted file mode 100644 index a3ddd08e7a45..000000000000 --- a/plugins/agent-workbench/prompt/core/src/AgentPromptContainerLauncher.kt +++ /dev/null @@ -1,21 +0,0 @@ -// 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.prompt.core - -import com.intellij.platform.ai.agent.core.session.AgentSessionProvider -import com.intellij.openapi.extensions.ExtensionPointName -import com.intellij.openapi.project.Project - -interface AgentPromptContainerLauncher { - fun isAvailable(): Boolean - fun supportsProvider(provider: AgentSessionProvider): Boolean = false - fun launch(project: Project, request: AgentPromptLaunchRequest) - - companion object { - @JvmField - val EP_NAME: ExtensionPointName = - ExtensionPointName("com.intellij.agent.workbench.containerLauncher") - - fun findInstance(): AgentPromptContainerLauncher? = - EP_NAME.extensionList.firstOrNull() - } -} diff --git a/plugins/agent-workbench/prompt/core/src/AgentPromptContextContributorBridge.kt b/plugins/agent-workbench/prompt/core/src/AgentPromptContextContributorBridge.kt deleted file mode 100644 index 60dc2a7ee9c1..000000000000 --- a/plugins/agent-workbench/prompt/core/src/AgentPromptContextContributorBridge.kt +++ /dev/null @@ -1,76 +0,0 @@ -// 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.prompt.core - -import com.intellij.platform.ai.agent.core.extensions.SnapshotExtensionPointCache -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.extensions.ExtensionPointName -import com.intellij.openapi.project.Project - -enum class AgentPromptContextContributorPhase { - INVOCATION, - FALLBACK, -} - -enum class AgentPromptDefaultContextPolicy { - ALLOW_IMPLICIT_CONTEXT, - NO_IMPLICIT_CONTEXT, -} - -data class AgentPromptInvocationData( - @JvmField val project: Project, - @JvmField val actionId: String?, - @JvmField val actionText: String?, - @JvmField val actionPlace: String?, - @JvmField val invokedAtMs: Long, - @JvmField val defaultContextPolicy: AgentPromptDefaultContextPolicy, - @JvmField val attributes: Map = emptyMap(), -) - -interface AgentPromptContextContributorBridge { - val phase: AgentPromptContextContributorPhase - get() = AgentPromptContextContributorPhase.INVOCATION - - fun collect(invocationData: AgentPromptInvocationData): List -} - -private class AgentPromptContextContributorBridgeRegistryLog - -private val LOG = logger() - -private val AGENT_PROMPT_CONTEXT_CONTRIBUTOR_BRIDGE_EP: ExtensionPointName = - ExtensionPointName("com.intellij.agent.workbench.promptContextContributor") - -private data class AgentPromptContextContributorBridgeSnapshot( - @JvmField val orderedContributors: List, -) { - companion object { - val EMPTY = AgentPromptContextContributorBridgeSnapshot(orderedContributors = emptyList()) - } -} - -private val CONTRIBUTOR_SNAPSHOT_CACHE = SnapshotExtensionPointCache( - log = LOG, - extensionPoint = AGENT_PROMPT_CONTEXT_CONTRIBUTOR_BRIDGE_EP, - cacheId = AgentPromptContextContributorBridgeSnapshot::class.java, - emptySnapshot = AgentPromptContextContributorBridgeSnapshot.EMPTY, - unavailableMessage = "Prompt context contributor EP is unavailable in this context", - buildSnapshot = ::buildAgentPromptContextContributorBridgeSnapshot, -) - -private fun buildAgentPromptContextContributorBridgeSnapshot( - contributors: Iterable, -): AgentPromptContextContributorBridgeSnapshot { - return AgentPromptContextContributorBridgeSnapshot( - orderedContributors = contributors.toList(), - ) -} - -private fun snapshotOrEmpty(): AgentPromptContextContributorBridgeSnapshot { - return CONTRIBUTOR_SNAPSHOT_CACHE.getSnapshotOrEmpty() -} - -object AgentPromptContextContributors { - fun allBridges(): List { - return snapshotOrEmpty().orderedContributors - } -} diff --git a/plugins/agent-workbench/prompt/core/src/AgentPromptContextEnvelopeFormatter.kt b/plugins/agent-workbench/prompt/core/src/AgentPromptContextEnvelopeFormatter.kt deleted file mode 100644 index c720ff092773..000000000000 --- a/plugins/agent-workbench/prompt/core/src/AgentPromptContextEnvelopeFormatter.kt +++ /dev/null @@ -1,341 +0,0 @@ -// 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.prompt.core - -import com.intellij.openapi.diagnostic.logger -import kotlin.math.max - -private const val CONTEXT_HEADER = "### IDE Context" -private const val SOFT_CAP_OMITTED_STUB = "[omitted due to soft cap]" -private const val SOFT_CAP_PARTIAL_SUFFIX = "\n...[truncated:soft_cap_partial]" -private const val MIN_PARTIAL_BODY_CHARS = 96 - -private class AgentPromptContextEnvelopeFormatterLog - -private val LOG = logger() - -object AgentPromptContextEnvelopeFormatter { - const val DEFAULT_SOFT_CAP_CHARS: Int = 12_000 - - data class ContextEnvelopeSelection( - @JvmField val items: List, - @JvmField val summary: AgentPromptContextEnvelopeSummary, - @JvmField val serializedChars: Int, - @JvmField val exceedsSoftCap: Boolean, - ) - - data class SoftCapTrimResult( - @JvmField val items: List, - @JvmField val serializedChars: Int, - @JvmField val exceedsSoftCap: Boolean, - ) - - fun prepareContextEnvelopeSelection( - items: List, - softCapChars: Int = DEFAULT_SOFT_CAP_CHARS, - projectPath: String? = null, - ): ContextEnvelopeSelection { - val summary = AgentPromptContextEnvelopeSummary( - softCapChars = softCapChars, - softCapExceeded = false, - autoTrimApplied = false, - ) - if (items.isEmpty()) { - return ContextEnvelopeSelection( - items = emptyList(), - summary = summary, - serializedChars = 0, - exceedsSoftCap = false, - ) - } - val normalizedItems = items.map { item -> normalizeItem(item) } - val serializedChars = measureContextBlockChars( - items = normalizedItems, - summary = summary, - projectPath = projectPath, - ) - return ContextEnvelopeSelection( - items = normalizedItems, - summary = summary, - serializedChars = serializedChars, - exceedsSoftCap = serializedChars > softCapChars, - ) - } - - fun markSoftCapExceeded(selection: ContextEnvelopeSelection): ContextEnvelopeSelection { - return selection.copy( - summary = selection.summary.copy( - softCapExceeded = true, - autoTrimApplied = false, - ), - exceedsSoftCap = true, - ) - } - - fun autoTrimContextEnvelopeSelection( - selection: ContextEnvelopeSelection, - projectPath: String? = null, - ): ContextEnvelopeSelection { - val trimResult = applySoftCap( - items = selection.items, - softCapChars = selection.summary.softCapChars, - projectPath = projectPath, - ) - return ContextEnvelopeSelection( - items = trimResult.items, - summary = selection.summary.copy( - softCapExceeded = true, - autoTrimApplied = true, - ), - serializedChars = trimResult.serializedChars, - exceedsSoftCap = trimResult.exceedsSoftCap, - ) - } - - fun composeInitialMessage(request: AgentPromptInitialMessageRequest): String { - val prompt = request.prompt.trim() - if (request.contextItems.isEmpty()) { - return prompt - } - - val summary = request.contextEnvelopeSummary ?: AgentPromptContextEnvelopeSummary( - softCapChars = DEFAULT_SOFT_CAP_CHARS, - softCapExceeded = false, - autoTrimApplied = false, - ) - val contextBlock = renderContextBlock( - items = request.contextItems, - summary = summary, - projectPath = request.projectPath, - ) - if (prompt.isEmpty()) { - return contextBlock - } - return buildString(prompt.length + contextBlock.length + 2) { - append(prompt) - append("\n\n") - append(contextBlock) - } - } - - fun measureContextBlockChars( - items: List, - summary: AgentPromptContextEnvelopeSummary = AgentPromptContextEnvelopeSummary(softCapChars = DEFAULT_SOFT_CAP_CHARS), - projectPath: String? = null, - ): Int { - return renderContextBlock(items = items, summary = summary, projectPath = projectPath).length - } - - fun renderContextBlock( - items: List, - summary: AgentPromptContextEnvelopeSummary, - projectPath: String? = null, - ): String { - val normalizedItems = items.map { item -> normalizeItem(item) } - return buildContextBlock(items = normalizedItems, summary = summary, projectPath = projectPath) - } - - fun renderContextItem(item: AgentPromptContextItem, projectPath: String? = null): String { - return renderNormalizedItem(item = normalizeItem(item), projectPath = projectPath).trimEnd() - } - - fun applySoftCap( - items: List, - softCapChars: Int = DEFAULT_SOFT_CAP_CHARS, - projectPath: String? = null, - ): SoftCapTrimResult { - if (items.isEmpty()) { - return SoftCapTrimResult(items = emptyList(), serializedChars = 0, exceedsSoftCap = false) - } - - val summary = AgentPromptContextEnvelopeSummary( - softCapChars = softCapChars, - softCapExceeded = true, - autoTrimApplied = true, - ) - val working = items.map { item -> normalizeItem(item) }.toMutableList() - - var serializedChars = measureContextBlockChars(items = working, summary = summary, projectPath = projectPath) - while (serializedChars > softCapChars) { - val updated = shrinkOneItem(items = working, excessChars = serializedChars - softCapChars) - if (!updated) { - break - } - serializedChars = measureContextBlockChars(items = working, summary = summary, projectPath = projectPath) - } - - return SoftCapTrimResult( - items = working, - serializedChars = serializedChars, - exceedsSoftCap = serializedChars > softCapChars, - ) - } - - private fun shrinkOneItem(items: MutableList, excessChars: Int): Boolean { - for (index in items.indices.reversed()) { - val current = items[index] - val body = current.body.trim() - if (body.isEmpty() || body == SOFT_CAP_OMITTED_STUB) { - continue - } - - val partiallyTrimmed = shrinkForSoftCap(body = body, excessChars = excessChars) - if (partiallyTrimmed != null) { - items[index] = updateTruncation( - item = current, - body = partiallyTrimmed, - reason = AgentPromptContextTruncationReason.SOFT_CAP_PARTIAL, - ) - return true - } - - items[index] = updateTruncation( - item = current, - body = SOFT_CAP_OMITTED_STUB, - reason = AgentPromptContextTruncationReason.SOFT_CAP_OMITTED, - ) - return true - } - return false - } - - private fun shrinkForSoftCap(body: String, excessChars: Int): String? { - val trimmedBody = body.trim() - if (trimmedBody.length <= MIN_PARTIAL_BODY_CHARS) { - return null - } - - val dynamicCut = max(excessChars, trimmedBody.length / 3) - val targetBodyLength = (trimmedBody.length - dynamicCut) - .coerceAtLeast(MIN_PARTIAL_BODY_CHARS) - if (targetBodyLength + SOFT_CAP_PARTIAL_SUFFIX.length >= trimmedBody.length) { - return null - } - - val nextBody = trimmedBody.take(targetBodyLength).trimEnd() - if (nextBody.length <= MIN_PARTIAL_BODY_CHARS) { - return null - } - return nextBody + SOFT_CAP_PARTIAL_SUFFIX - } - - private fun normalizeItem(item: AgentPromptContextItem): AgentPromptContextItem { - val normalizedBody = item.body.trim() - val normalizedTruncation = normalizeTruncation(item = item, normalizedBody = normalizedBody) - return item.copy( - body = normalizedBody, - truncation = normalizedTruncation, - ) - } - - private fun normalizeTruncation(item: AgentPromptContextItem, normalizedBody: String): AgentPromptContextTruncation { - val base = item.truncation - val normalizedOriginal = base.originalChars - .coerceAtLeast(normalizedBody.length) - .coerceAtLeast(0) - val normalizedIncluded = base.includedChars - .coerceAtLeast(0) - .coerceAtMost(normalizedOriginal) - .coerceAtLeast(normalizedBody.length.coerceAtMost(normalizedOriginal)) - val reason = if (normalizedIncluded < normalizedOriginal && base.reason == AgentPromptContextTruncationReason.NONE) { - AgentPromptContextTruncationReason.SOURCE_LIMIT - } - else { - base.reason - } - return AgentPromptContextTruncation( - originalChars = normalizedOriginal, - includedChars = normalizedIncluded, - reason = reason, - ) - } - - private fun updateTruncation( - item: AgentPromptContextItem, - body: String, - reason: AgentPromptContextTruncationReason, - ): AgentPromptContextItem { - val normalizedBody = body.trim() - val originalChars = max( - item.truncation.originalChars, - max(normalizedBody.length, item.body.length), - ) - val truncation = AgentPromptContextTruncation( - originalChars = originalChars, - includedChars = normalizedBody.length, - reason = reason, - ) - return item.copy( - body = normalizedBody, - truncation = truncation, - ) - } - - private fun buildContextBlock( - items: List, - summary: AgentPromptContextEnvelopeSummary, - projectPath: String?, - ): String { - val normalizedSummary = summary.normalized() - val builder = StringBuilder(items.sumOf { item -> item.body.length } + 128) - builder.append(CONTEXT_HEADER) - if (normalizedSummary.softCapExceeded) { - builder.append('\n') - .append("soft-cap: limit=") - .append(normalizedSummary.softCapChars) - .append(" auto-trim=") - .append(booleanAsYesNo(normalizedSummary.autoTrimApplied)) - } - if (items.isNotEmpty()) { - builder.append("\n\n") - } - - items.forEachIndexed { index, item -> - if (index > 0) { - builder.append('\n') - } - builder.append(renderNormalizedItem(item = item, projectPath = projectPath).trimEnd()) - builder.append('\n') - } - - return builder.toString().trimEnd() - } - - private fun renderNormalizedItem(item: AgentPromptContextItem, projectPath: String?): String { - val renderer = AgentPromptContextRenderers.find(item.rendererId) - if (renderer != null) { - return try { - renderer.renderEnvelope( - AgentPromptEnvelopeRenderInput( - item = item, - projectPath = projectPath, - ) - ) - } - catch (t: Throwable) { - LOG.warn("Prompt context renderer failed: ${renderer::class.java.name} (rendererId=${item.rendererId})", t) - renderGeneric(item) - } - } - return renderGeneric(item) - } - - private fun renderGeneric(item: AgentPromptContextItem): String { - val title = item.title?.takeIf { it.isNotBlank() } ?: "Context" - val descriptor = "context: renderer=${item.rendererId} title=$title${renderTruncationSuffix(item)}" - return descriptor + "\n" + appendCodeBlock(language = "text", content = item.body) - } - - private fun booleanAsYesNo(value: Boolean): String { - return if (value) "yes" else "no" - } - - private fun AgentPromptContextEnvelopeSummary.normalized(): AgentPromptContextEnvelopeSummary { - val normalizedSoftCap = softCapChars.coerceAtLeast(1) - return if (normalizedSoftCap == softCapChars) { - this - } - else { - copy(softCapChars = normalizedSoftCap) - } - } -} diff --git a/plugins/agent-workbench/prompt/core/src/AgentPromptContextPayloads.kt b/plugins/agent-workbench/prompt/core/src/AgentPromptContextPayloads.kt deleted file mode 100644 index 4ea21220b6d0..000000000000 --- a/plugins/agent-workbench/prompt/core/src/AgentPromptContextPayloads.kt +++ /dev/null @@ -1,51 +0,0 @@ -// 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.prompt.core - -object AgentPromptContextRendererIds { - const val SNIPPET: String = "snippet" - const val FILE: String = "file" - const val SYMBOL: String = "symbol" - const val PATHS: String = "paths" - const val VCS_COMMITS: String = "vcsCommits" - const val TEST_FAILURES: String = "testFailures" -} - -object AgentPromptContextItemIds { - const val CHANGES_SELECTION: String = "changes.selection" -} - -object AgentPromptPayload { - fun obj(vararg entries: Pair): AgentPromptPayloadValue.Obj { - return AgentPromptPayloadValue.Obj(linkedMapOf(*entries)) - } - - fun arr(vararg items: AgentPromptPayloadValue): AgentPromptPayloadValue.Arr { - return AgentPromptPayloadValue.Arr(items.toList()) - } - - fun str(value: String): AgentPromptPayloadValue.Str = AgentPromptPayloadValue.Str(value) - - fun num(value: Int): AgentPromptPayloadValue.Num = AgentPromptPayloadValue.Num(value.toString()) - - fun bool(value: Boolean): AgentPromptPayloadValue.Bool = AgentPromptPayloadValue.Bool(value) -} - -fun AgentPromptPayloadValue.objOrNull(): AgentPromptPayloadValue.Obj? { - return this as? AgentPromptPayloadValue.Obj -} - -fun AgentPromptPayloadValue.Obj.string(name: String): String? { - return (fields[name] as? AgentPromptPayloadValue.Str)?.value -} - -fun AgentPromptPayloadValue.Obj.bool(name: String): Boolean? { - return (fields[name] as? AgentPromptPayloadValue.Bool)?.value -} - -fun AgentPromptPayloadValue.Obj.number(name: String): String? { - return (fields[name] as? AgentPromptPayloadValue.Num)?.value -} - -fun AgentPromptPayloadValue.Obj.array(name: String): List? { - return (fields[name] as? AgentPromptPayloadValue.Arr)?.items -} diff --git a/plugins/agent-workbench/prompt/core/src/AgentPromptContextRendererBridge.kt b/plugins/agent-workbench/prompt/core/src/AgentPromptContextRendererBridge.kt deleted file mode 100644 index 3c27c4a679be..000000000000 --- a/plugins/agent-workbench/prompt/core/src/AgentPromptContextRendererBridge.kt +++ /dev/null @@ -1,131 +0,0 @@ -// 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.prompt.core - -import com.intellij.platform.ai.agent.core.extensions.OverridableValue -import com.intellij.platform.ai.agent.core.extensions.SnapshotExtensionPointCache -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.extensions.ExtensionPointName - -data class AgentPromptEnvelopeRenderInput( - @JvmField val item: AgentPromptContextItem, - @JvmField val projectPath: String?, -) - -data class AgentPromptChipRenderInput( - @JvmField val item: AgentPromptContextItem, - @JvmField val projectBasePath: String?, -) - -data class AgentPromptChipRender( - @JvmField val text: String, - @JvmField val tooltipText: String? = null, -) - -interface AgentPromptContextRendererBridge { - val rendererId: String - - fun renderEnvelope(input: AgentPromptEnvelopeRenderInput): String - - fun renderChip(input: AgentPromptChipRenderInput): AgentPromptChipRender? { - return null - } -} - -private class AgentPromptContextRendererRegistryLog - -private val LOG = logger() - -private val AGENT_PROMPT_CONTEXT_RENDERER_BRIDGE_EP: ExtensionPointName = - ExtensionPointName("com.intellij.agent.workbench.promptContextRenderer") - -private data class AgentPromptContextRendererBridgeSnapshot( - @JvmField val orderedBridges: List, - @JvmField val bridgesById: Map, -) { - companion object { - val EMPTY = AgentPromptContextRendererBridgeSnapshot( - orderedBridges = emptyList(), - bridgesById = emptyMap(), - ) - } -} - -private val RENDERER_SNAPSHOT_CACHE = SnapshotExtensionPointCache( - log = LOG, - extensionPoint = AGENT_PROMPT_CONTEXT_RENDERER_BRIDGE_EP, - cacheId = AgentPromptContextRendererBridgeSnapshot::class.java, - emptySnapshot = AgentPromptContextRendererBridgeSnapshot.EMPTY, - unavailableMessage = "Prompt context renderer EP is unavailable in this context", - buildSnapshot = ::buildAgentPromptContextRendererBridgeSnapshot, -) - -interface AgentPromptContextRendererRegistry { - fun allBridges(): List - - fun find(rendererId: String): AgentPromptContextRendererBridge? -} - -private fun buildAgentPromptContextRendererBridgeSnapshot( - bridges: Iterable, -): AgentPromptContextRendererBridgeSnapshot { - val ordered = ArrayList() - val byId = LinkedHashMap() - for (bridge in bridges) { - val existing = byId.putIfAbsent(bridge.rendererId, bridge) - if (existing == null) { - ordered += bridge - } - else if (existing !== bridge) { - LOG.warn( - "Duplicate prompt context renderer '${bridge.rendererId}': keeping ${existing::class.java.name}, ignoring ${bridge::class.java.name}", - ) - } - } - return AgentPromptContextRendererBridgeSnapshot( - orderedBridges = ordered, - bridgesById = byId, - ) -} - -private class EpBackedAgentPromptContextRendererRegistry : AgentPromptContextRendererRegistry { - override fun allBridges(): List { - return snapshotOrEmpty().orderedBridges - } - - override fun find(rendererId: String): AgentPromptContextRendererBridge? { - return snapshotOrEmpty().bridgesById[rendererId] - } -} - -private fun snapshotOrEmpty(): AgentPromptContextRendererBridgeSnapshot { - return RENDERER_SNAPSHOT_CACHE.getSnapshotOrEmpty() -} - -@Suppress("unused") -class InMemoryAgentPromptContextRendererRegistry( - bridges: Iterable, -) : AgentPromptContextRendererRegistry { - private val snapshot = buildAgentPromptContextRendererBridgeSnapshot(bridges) - - override fun allBridges(): List { - return snapshot.orderedBridges - } - - override fun find(rendererId: String): AgentPromptContextRendererBridge? { - return snapshot.bridgesById[rendererId] - } -} - -object AgentPromptContextRenderers { - private val epRegistry: AgentPromptContextRendererRegistry = EpBackedAgentPromptContextRendererRegistry() - private val registryOverride = OverridableValue { epRegistry } - - fun find(rendererId: String): AgentPromptContextRendererBridge? { - return registryOverride.value().find(rendererId) - } - - @Suppress("unused") - fun withRegistryForTest(registry: AgentPromptContextRendererRegistry, action: () -> T): T { - return registryOverride.withOverride(registry, action) - } -} diff --git a/plugins/agent-workbench/prompt/core/src/AgentPromptContextResolverService.kt b/plugins/agent-workbench/prompt/core/src/AgentPromptContextResolverService.kt deleted file mode 100644 index 506c1d2d44c4..000000000000 --- a/plugins/agent-workbench/prompt/core/src/AgentPromptContextResolverService.kt +++ /dev/null @@ -1,67 +0,0 @@ -// 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.prompt.core - -import com.intellij.openapi.components.Service -import com.intellij.openapi.diagnostic.logger - -private class AgentPromptContextResolverServiceLog - -private val LOG = logger() - -@Service(Service.Level.PROJECT) -class AgentPromptContextResolverService( - private val contributorsProvider: () -> List = AgentPromptContextContributors::allBridges, -) { - fun collectDefaultContext(invocationData: AgentPromptInvocationData): List { - if (invocationData.defaultContextPolicy != AgentPromptDefaultContextPolicy.ALLOW_IMPLICIT_CONTEXT) { - return emptyList() - } - - val invocationItems = collectByPhase( - phase = AgentPromptContextContributorPhase.INVOCATION, - invocationData = invocationData, - ) - if (invocationItems.isNotEmpty()) { - return invocationItems - } - - return collectByPhase( - phase = AgentPromptContextContributorPhase.FALLBACK, - invocationData = invocationData, - ) - } - - private fun collectByPhase( - phase: AgentPromptContextContributorPhase, - invocationData: AgentPromptInvocationData, - ): List { - val contributors = contributorsProvider().filter { contributor -> contributor.phase == phase } - contributors.forEach { contributor -> - val items = try { - contributor.collect(invocationData) - } - catch (t: Throwable) { - LOG.warn("Prompt context contributor failed: ${contributor::class.java.name}", t) - emptyList() - } - if (items.isNotEmpty()) { - return appendPhaseMetadata(items, phase) - } - } - return emptyList() - } - - private fun appendPhaseMetadata( - items: List, - phase: AgentPromptContextContributorPhase, - ): List { - return items.map { item -> - if (item.phase == phase) { - item - } - else { - item.copy(phase = phase) - } - } - } -} diff --git a/plugins/agent-workbench/prompt/core/src/AgentPromptInvocationDataAttributes.kt b/plugins/agent-workbench/prompt/core/src/AgentPromptInvocationDataAttributes.kt deleted file mode 100644 index 94a1b7280ba7..000000000000 --- a/plugins/agent-workbench/prompt/core/src/AgentPromptInvocationDataAttributes.kt +++ /dev/null @@ -1,25 +0,0 @@ -// 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.prompt.core - -import com.intellij.openapi.actionSystem.DataContext -import com.intellij.openapi.actionSystem.DataKey -import com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode - -const val AGENT_PROMPT_INVOCATION_DATA_CONTEXT_KEY: String = "dataContext" -const val AGENT_PROMPT_INVOCATION_PREFER_EXTENSIONS_KEY: String = "preferExtensionTabs" - -val AGENT_PROMPT_INITIAL_TEXT_DATA_KEY: DataKey = DataKey.create("AgentPrompt.initialText") -val AGENT_PROMPT_MESSAGE_REQUEST_DATA_KEY: DataKey = DataKey.create("AgentPrompt.messageRequest") -val AGENT_PROMPT_SELECTED_PROVIDER_ID_DATA_KEY: DataKey = DataKey.create("AgentPrompt.selectedProviderId") -val AGENT_PROMPT_SELECTED_LAUNCH_MODE_DATA_KEY: DataKey = DataKey.create("AgentPrompt.selectedLaunchMode") - -/** Launch generation settings (model, reasoning effort) chosen in the prompt palette for an extension submit action. */ -val AGENT_PROMPT_GENERATION_SETTINGS_DATA_KEY: DataKey = DataKey.create("AgentPrompt.generationSettings") - -/** Provider model catalog backing [AGENT_PROMPT_GENERATION_SETTINGS_DATA_KEY], used to resolve the selected model on launch. */ -val AGENT_PROMPT_GENERATION_MODEL_CATALOG_DATA_KEY: DataKey> = - DataKey.create("AgentPrompt.generationModelCatalog") - -fun AgentPromptInvocationData.dataContextOrNull(): DataContext? { - return attributes[AGENT_PROMPT_INVOCATION_DATA_CONTEXT_KEY] as? DataContext -} diff --git a/plugins/agent-workbench/prompt/core/src/AgentPromptLauncherBridge.kt b/plugins/agent-workbench/prompt/core/src/AgentPromptLauncherBridge.kt deleted file mode 100644 index 1f60f243dab9..000000000000 --- a/plugins/agent-workbench/prompt/core/src/AgentPromptLauncherBridge.kt +++ /dev/null @@ -1,102 +0,0 @@ -// 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.prompt.core - -import com.intellij.platform.ai.agent.core.extensions.OverridableValue -import com.intellij.platform.ai.agent.core.extensions.SingleExtensionPointResolver -import com.intellij.platform.ai.agent.core.session.AgentSessionProvider -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.extensions.ExtensionPointName -import com.intellij.openapi.project.Project -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf - -interface AgentPromptLauncherBridge { - suspend fun launch(request: AgentPromptLaunchRequest): AgentPromptLaunchResult - - fun loadProviderPreferences(): ProviderPreferences { - return ProviderPreferences() - } - - fun saveProviderPreferences(preferences: ProviderPreferences) { - } - - - fun observeExistingThreads( - projectPath: String, - provider: AgentSessionProvider, - ): Flow { - return flowOf( - AgentPromptExistingThreadsSnapshot( - threads = emptyList(), - isLoading = false, - hasLoaded = true, - hasError = false, - ) - ) - } - - suspend fun refreshExistingThreads(projectPath: String, provider: AgentSessionProvider) { - } - - fun resolveWorkingProjectPath(invocationData: AgentPromptInvocationData): String? { - return null - } - - fun resolveSourceProject(invocationData: AgentPromptInvocationData): Project? { - return invocationData.project - } - - fun listWorkingProjectPathCandidates(invocationData: AgentPromptInvocationData): List { - return emptyList() - } - - suspend fun listAddContextTargetCandidates(projectPath: String): List { - return emptyList() - } - - suspend fun addContextToOpenThreadViewTarget(request: AgentPromptAddContextToTargetRequest): AgentPromptAddContextToTargetResult { - return AgentPromptAddContextToTargetResult.UNAVAILABLE - } - - suspend fun listReusablePromptSourceEntries( - projectPath: String, - provider: AgentSessionProvider, - ): List { - return emptyList() - } - - data class ProviderPreferences( - @JvmField val providerOptionsByProviderId: Map> = emptyMap(), - @JvmField val containerModeEnabled: Boolean = false, - @JvmField val launchProfiles: List = emptyList(), - @JvmField val defaultLaunchProfileId: String? = null, - ) -} - -private class AgentPromptLauncherBridgeLog - -private val LOG = logger() -private val AGENT_PROMPT_LAUNCHER_BRIDGE_EP: ExtensionPointName = - ExtensionPointName("com.intellij.agent.workbench.promptLauncher") - -private val REGISTERED_PROMPT_LAUNCHER = SingleExtensionPointResolver( - log = LOG, - extensionPoint = AGENT_PROMPT_LAUNCHER_BRIDGE_EP, - unavailableMessage = "Prompt launcher bridge EP is unavailable in this context", - multipleExtensionsMessage = { launchers -> - "Multiple prompt launchers registered; using first: ${launchers.map { it::class.java.name }}" - }, -) - -object AgentPromptLaunchers { - private val launcherOverride = OverridableValue { REGISTERED_PROMPT_LAUNCHER.findFirstOrNull() } - - fun find(): AgentPromptLauncherBridge? { - return launcherOverride.value() - } - - @Suppress("unused") - fun withLauncherForTest(launcher: AgentPromptLauncherBridge?, action: () -> T): T { - return launcherOverride.withOverride(launcher, action) - } -} diff --git a/plugins/agent-workbench/prompt/core/src/AgentPromptManualContextSourceBridge.kt b/plugins/agent-workbench/prompt/core/src/AgentPromptManualContextSourceBridge.kt deleted file mode 100644 index 7c05a771302f..000000000000 --- a/plugins/agent-workbench/prompt/core/src/AgentPromptManualContextSourceBridge.kt +++ /dev/null @@ -1,49 +0,0 @@ -// 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.prompt.core - -import com.intellij.openapi.extensions.ExtensionPointName -import com.intellij.openapi.project.Project -import org.jetbrains.annotations.Nls -import java.awt.Component - -data class AgentPromptManualContextPickerRequest( - @JvmField val hostProject: Project, - @JvmField val sourceProject: Project, - @JvmField val invocationData: AgentPromptInvocationData, - @JvmField val workingProjectPath: String?, - @JvmField val currentItems: List = emptyList(), - @JvmField val anchorComponent: Component, - @JvmField val onSelected: (AgentPromptContextItem) -> Unit, - @JvmField val onError: (@Nls String) -> Unit, -) { - val currentItem: AgentPromptContextItem? - get() = currentItems.firstOrNull() -} - -enum class AgentPromptManualContextSelectionMode { - REPLACE, - APPEND, -} - -interface AgentPromptManualContextSourceBridge { - val sourceId: String - - val order: Int - get() = Int.MAX_VALUE - - val selectionMode: AgentPromptManualContextSelectionMode - get() = AgentPromptManualContextSelectionMode.REPLACE - - fun isAvailable(project: Project): Boolean = true - - fun getDisplayName(): @Nls String - - fun showPicker(request: AgentPromptManualContextPickerRequest) -} - -object AgentPromptManualContextSources { - private val EP_NAME: ExtensionPointName = - ExtensionPointName("com.intellij.agent.workbench.promptManualContextSource") - - fun allSources(): List = EP_NAME.extensionList -} diff --git a/plugins/agent-workbench/prompt/core/src/AgentPromptModels.kt b/plugins/agent-workbench/prompt/core/src/AgentPromptModels.kt deleted file mode 100644 index 29937e761692..000000000000 --- a/plugins/agent-workbench/prompt/core/src/AgentPromptModels.kt +++ /dev/null @@ -1,234 +0,0 @@ -// 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.prompt.core - -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 -import com.intellij.openapi.util.NlsSafe -import kotlinx.serialization.Serializable - -data class AgentPromptContextItem( - @JvmField val rendererId: String, - @JvmField val title: String?, - @JvmField val body: String, - @JvmField val payload: AgentPromptPayloadValue = AgentPromptPayloadValue.Obj.EMPTY, - @JvmField val itemId: String? = null, - @JvmField val parentItemId: String? = null, - @JvmField val source: String = "unknown", - @JvmField val phase: AgentPromptContextContributorPhase? = null, - @JvmField val truncation: AgentPromptContextTruncation = AgentPromptContextTruncation.none(body.length), -) - -sealed interface AgentPromptPayloadValue { - data class Obj( - @JvmField val fields: Map, - ) : AgentPromptPayloadValue { - companion object { - @JvmField - val EMPTY: Obj = Obj(emptyMap()) - } - } - - data class Arr( - @JvmField val items: List, - ) : AgentPromptPayloadValue - - data class Str( - @JvmField val value: String, - ) : AgentPromptPayloadValue - - data class Num( - @JvmField val value: String, - ) : AgentPromptPayloadValue - - data class Bool( - @JvmField val value: Boolean, - ) : AgentPromptPayloadValue - - data object Null : AgentPromptPayloadValue -} - -enum class AgentPromptContextTruncationReason { - NONE, - SOURCE_LIMIT, - SOFT_CAP_PARTIAL, - SOFT_CAP_OMITTED, -} - -data class AgentPromptContextTruncation( - @JvmField val originalChars: Int, - @JvmField val includedChars: Int, - @JvmField val reason: AgentPromptContextTruncationReason, -) { - companion object { - fun none(chars: Int): AgentPromptContextTruncation { - return AgentPromptContextTruncation( - originalChars = chars.coerceAtLeast(0), - includedChars = chars.coerceAtLeast(0), - reason = AgentPromptContextTruncationReason.NONE, - ) - } - } -} - -data class AgentPromptInitialMessageRequest( - @JvmField val prompt: String, - @JvmField val projectPath: String? = null, - @JvmField val contextItems: List = emptyList(), - @JvmField val contextEnvelopeSummary: AgentPromptContextEnvelopeSummary? = null, - @JvmField val providerOptionIds: Set = emptySet(), -) - -@Serializable -enum class AgentPromptReasoningEffort { - AUTO, - LOW, - MEDIUM, - HIGH, - XHIGH, - MAX, -} - -@Serializable -data class AgentPromptGenerationSettings( - @JvmField val modelId: String? = null, - @JvmField val reasoningEffort: AgentPromptReasoningEffort = AgentPromptReasoningEffort.AUTO, - @JvmField val planReasoningEffort: AgentPromptReasoningEffort? = null, -) { - companion object { - @JvmField - val AUTO: AgentPromptGenerationSettings = AgentPromptGenerationSettings() - } -} - -@Serializable -enum class AgentPromptLaunchProfileKind { - BUILT_IN, - USER, -} - -@Serializable -data class AgentPromptLaunchProfile( - @JvmField val id: String, - @JvmField val name: @NlsSafe String, - @JvmField val kind: AgentPromptLaunchProfileKind = AgentPromptLaunchProfileKind.USER, - @JvmField val providerId: String, - @JvmField val launchMode: AgentSessionLaunchMode = AgentSessionLaunchMode.STANDARD, - @JvmField val launchTargetId: String? = null, - @JvmField val surfaceId: String? = null, - @JvmField val generationSettings: AgentPromptGenerationSettings = AgentPromptGenerationSettings.AUTO, -) - -data class AgentPromptGenerationModel( - @JvmField val id: String, - @JvmField val displayName: @NlsSafe String, - @JvmField val supportedReasoningEfforts: Set = emptySet(), - @JvmField val defaultReasoningEffort: AgentPromptReasoningEffort? = null, - @JvmField val isDefault: Boolean = false, -) { - @JvmField - var group: AgentPromptGenerationModelGroup = AgentPromptGenerationModelGroup.OTHER -} - -fun AgentPromptGenerationModel.withGroup(group: AgentPromptGenerationModelGroup): AgentPromptGenerationModel { - return copy().also { model -> model.group = group } -} - -enum class AgentPromptGenerationModelGroup { - LOCAL, - OPENAI, - CLAUDE_CODE, - OTHER, -} - -data class AgentPromptContextEnvelopeSummary( - @JvmField val softCapChars: Int = 12_000, - @JvmField val softCapExceeded: Boolean = false, - @JvmField val autoTrimApplied: Boolean = false, -) - -data class AgentPromptLaunchRequest( - @JvmField val launchProfileId: String? = null, - val provider: AgentSessionProvider, - @JvmField val projectPath: String, - @JvmField val launchMode: AgentSessionLaunchMode, - @JvmField val launchTargetId: String? = null, - @JvmField val surfaceId: String? = null, - @JvmField val initialMessageRequest: AgentPromptInitialMessageRequest, - @JvmField val targetThreadId: String? = null, - @JvmField val preferredDedicatedFrame: Boolean? = null, - @JvmField val generationSettings: AgentPromptGenerationSettings = AgentPromptGenerationSettings.AUTO, - @JvmField val generationModelCatalog: List = emptyList(), - @JvmField val containerMode: Boolean = false, - /** - * Extra environment variables to inject into the terminal launch spec. - * Used by the EEL container flow to pass `AGENT_CONTAINER_SESSION_ID` and - * `AGENT_CONTAINER_WORKSPACE_PATH` to the Claude Code process on the host. - * Merged into the launch spec after standard augmentation. - */ - @JvmField val containerSessionEnvVariables: Map = emptyMap(), - /** - * Extra command-line arguments appended to the agent CLI command. - * Used by the EEL container flow to add `--disallowedTools` so that - * Claude Code's built-in file/bash tools are disabled, forcing all - * operations through the ij-proxy MCP tools (which route to the container). - */ - @JvmField val containerSessionExtraArgs: List = emptyList(), -) - -data class AgentPromptProjectPathCandidate( - @JvmField val path: @NlsSafe String, - @JvmField val displayName: @NlsSafe String, -) - -data class AgentPromptAddContextTargetCandidate( - @JvmField val projectPath: @NlsSafe String, - val provider: AgentSessionProvider, - @JvmField val launchMode: AgentSessionLaunchMode = AgentSessionLaunchMode.STANDARD, - @JvmField val threadId: @NlsSafe String, - @JvmField val displayText: @NlsSafe String, - @JvmField val secondaryText: @NlsSafe String = "", - @JvmField val selected: Boolean = false, -) - -data class AgentPromptAddContextToTargetRequest( - @JvmField val target: AgentPromptAddContextTargetCandidate, - @JvmField val contextItems: List, -) - -enum class AgentPromptAddContextToTargetResult { - ADDED_TO_THREAD_VIEW, - ALREADY_ADDED_TO_THREAD_VIEW, - UNAVAILABLE, -} - -enum class AgentPromptLaunchError { - PROVIDER_UNAVAILABLE, - UNSUPPORTED_LAUNCH_MODE, - TARGET_THREAD_NOT_FOUND, - TARGET_THREAD_BUSY_FOR_PLAN_MODE, - CANCELLED, - DROPPED_DUPLICATE, - INTERNAL_ERROR, -} - -data class AgentPromptLaunchResult( - @JvmField val launched: Boolean, - @JvmField val error: AgentPromptLaunchError? = null, -) { - companion object { - @JvmField - val SUCCESS: AgentPromptLaunchResult = AgentPromptLaunchResult(launched = true) - - fun failure(error: AgentPromptLaunchError): AgentPromptLaunchResult { - return AgentPromptLaunchResult(launched = false, error = error) - } - } -} - -data class AgentPromptExistingThreadsSnapshot( - @JvmField val threads: List, - @JvmField val isLoading: Boolean, - @JvmField val hasLoaded: Boolean, - @JvmField val hasError: Boolean, -) diff --git a/plugins/agent-workbench/prompt/core/src/AgentPromptPaletteExtension.kt b/plugins/agent-workbench/prompt/core/src/AgentPromptPaletteExtension.kt deleted file mode 100644 index d3f58404784e..000000000000 --- a/plugins/agent-workbench/prompt/core/src/AgentPromptPaletteExtension.kt +++ /dev/null @@ -1,113 +0,0 @@ -// 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.prompt.core - -import com.intellij.openapi.extensions.ExtensionPointName -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Key -import org.jetbrains.annotations.Nls - -data class AgentPromptPaletteInitialPrompt( - val kind: String? = null, - val content: String = "", -) - -/** - * Extension point that allows contributing custom tabs to the prompt palette popup. - * - * When context items match (e.g., Commits), a dynamic tab is added with the given title. - * The extension can provide initial prompt text, a custom submit action, and a footer hint. - */ -interface AgentPromptPaletteExtension { - /** - * Returns `true` if this extension's tab should appear for the given context items. - */ - fun matches(contextItems: List): Boolean - - /** - * Returns the tab title displayed in the tabbed pane header. - */ - fun getTabTitle(): @Nls String - - /** - * Returns initial prompt to pre-fill in the prompt area when the tab is selected, or `null` to leave it empty. - */ - fun getInitialPrompt(project: Project): AgentPromptPaletteInitialPrompt? - - /** - * Classifies the provided draft text for persistence. - * - * Implementations may return a different kind than [getInitialPrompt] when the user's edits move the draft - * between logical prompt templates. - */ - fun classifyPromptDraftKind(project: Project, promptText: String): String? = getInitialPrompt(project)?.kind - - /** - * Synchronizes the current draft prompt with the latest context for this extension. - * - * Implementations should preserve user-authored text whenever possible and only update - * context-derived fragments that are owned by the extension. - */ - fun synchronizePrompt(project: Project, currentPrompt: AgentPromptPaletteInitialPrompt): AgentPromptPaletteInitialPrompt = currentPrompt - - /** - * Returns the action ID to invoke on submit instead of the normal launch flow, - * or `null` to use the default behavior. - */ - fun getSubmitActionId(): String? - - /** - * Returns `true` if the prompt palette should keep the provider selector visible while this tab is active, - * so the user can choose which Agent Workbench provider the [getSubmitActionId] action runs. - * - * Extension tabs otherwise bypass provider routing and hide the selector. The selected provider id is always - * forwarded to the submit action through the data context regardless of this flag. - */ - fun showsProviderSelector(): Boolean = false - - /** - * Returns `true` if the prompt palette should also show the per-task generation controls (model and - * reasoning effort) while this tab is active, matching the standard `New Task` launch controls. Requires - * [showsProviderSelector] so a provider is chosen for the model catalog. The selected settings are forwarded - * to the submit action through the data context. - */ - fun showsGenerationControls(): Boolean = false - - /** - * Returns a custom footer hint to display when this tab is active, - * or `null` to use the default hint. - */ - fun getFooterHint(): @Nls String? - - /** - * Returns `true` if this extension's tab should be auto-selected when the popup - * is invoked with the "prefer extensions" shortcut and this extension [matches] the context. - * When multiple extensions return `true`, the first one wins. - */ - fun shouldAutoSelect(contextItems: List): Boolean = matches(contextItems) -} - -object AgentPromptPaletteExtensions { - private val EP_NAME: ExtensionPointName = - ExtensionPointName("com.intellij.agent.workbench.promptPaletteExtension") - - fun allExtensions(): List = EP_NAME.extensionList -} - -object AgentPromptPaletteExtensionContext { - private val CONTEXT_ITEMS_KEY: Key> = Key.create("agent.workbench.prompt.palette.contextItems") - - fun withContextItems(project: Project, contextItems: List, action: () -> T): T { - val previousItems = project.getUserData(CONTEXT_ITEMS_KEY) - project.putUserData(CONTEXT_ITEMS_KEY, contextItems) - return try { - action() - } - finally { - project.putUserData(CONTEXT_ITEMS_KEY, previousItems) - } - } - - fun getContextItems(project: Project): List { - return project.getUserData(CONTEXT_ITEMS_KEY).orEmpty() - } -} diff --git a/plugins/agent-workbench/prompt/core/src/AgentPromptProjectPathContext.kt b/plugins/agent-workbench/prompt/core/src/AgentPromptProjectPathContext.kt deleted file mode 100644 index fdb9310793b8..000000000000 --- a/plugins/agent-workbench/prompt/core/src/AgentPromptProjectPathContext.kt +++ /dev/null @@ -1,20 +0,0 @@ -// 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.prompt.core - -import com.intellij.openapi.actionSystem.DataContext -import com.intellij.openapi.actionSystem.DataKey - -const val AGENT_PROMPT_PROJECT_PATH_CONTEXT_DATA_ID: String = "agent.workbench.prompt.projectPathContext" - -data class AgentPromptProjectPathContext( - @JvmField val path: String, - @JvmField val displayName: String? = null, -) - -@JvmField -val AGENT_PROMPT_PROJECT_PATH_CONTEXT_DATA_KEY: DataKey = - DataKey.create(AGENT_PROMPT_PROJECT_PATH_CONTEXT_DATA_ID) - -fun getAgentPromptProjectPathContext(dataContext: DataContext?): AgentPromptProjectPathContext? { - return dataContext?.getData(AGENT_PROMPT_PROJECT_PATH_CONTEXT_DATA_KEY) -} diff --git a/plugins/agent-workbench/prompt/core/src/AgentPromptReusableSourceEntry.kt b/plugins/agent-workbench/prompt/core/src/AgentPromptReusableSourceEntry.kt deleted file mode 100644 index b2a8fc632b14..000000000000 --- a/plugins/agent-workbench/prompt/core/src/AgentPromptReusableSourceEntry.kt +++ /dev/null @@ -1,21 +0,0 @@ -// 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.prompt.core - -import com.intellij.platform.ai.agent.core.session.AgentSessionProvider -import com.intellij.openapi.util.NlsSafe - -enum class AgentPromptReusableSourceKind { - PROMPT_FILE, - COMMAND, - SKILL, -} - -data class AgentPromptReusableSourceEntry( - @JvmField val id: String, - @JvmField val label: @NlsSafe String, - @JvmField val insertText: @NlsSafe String, - @JvmField val kind: AgentPromptReusableSourceKind, - val provider: AgentSessionProvider? = null, - @JvmField val description: @NlsSafe String? = null, - @JvmField val sourcePath: String? = null, -) diff --git a/plugins/agent-workbench/prompt/core/src/AgentPromptSuggestionBundle.kt b/plugins/agent-workbench/prompt/core/src/AgentPromptSuggestionBundle.kt deleted file mode 100644 index 230c4291d5bc..000000000000 --- a/plugins/agent-workbench/prompt/core/src/AgentPromptSuggestionBundle.kt +++ /dev/null @@ -1,19 +0,0 @@ -// 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.prompt.core - -// @spec plugins/air/spec/actions/global-prompt-suggestions.spec.md - -import com.intellij.DynamicBundle -import org.jetbrains.annotations.Nls -import org.jetbrains.annotations.NonNls -import org.jetbrains.annotations.PropertyKey - -const val AGENT_PROMPT_SUGGESTIONS_BUNDLE: @NonNls String = "messages.AgentPromptSuggestionsBundle" - -object AgentPromptSuggestionBundle { - private val BUNDLE = DynamicBundle(AgentPromptSuggestionBundle::class.java, AGENT_PROMPT_SUGGESTIONS_BUNDLE) - - fun message(key: @PropertyKey(resourceBundle = AGENT_PROMPT_SUGGESTIONS_BUNDLE) String, vararg params: Any): @Nls String { - return BUNDLE.getMessage(key, *params) - } -} diff --git a/plugins/agent-workbench/prompt/core/src/AgentPromptSuggestionGenerator.kt b/plugins/agent-workbench/prompt/core/src/AgentPromptSuggestionGenerator.kt deleted file mode 100644 index 7d4c25b26c27..000000000000 --- a/plugins/agent-workbench/prompt/core/src/AgentPromptSuggestionGenerator.kt +++ /dev/null @@ -1,296 +0,0 @@ -// 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.prompt.core - -// @spec plugins/air/spec/actions/global-prompt-suggestions.spec.md - -import com.intellij.platform.ai.agent.core.extensions.OverridableValue -import com.intellij.platform.ai.agent.core.extensions.SingleExtensionPointResolver -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.extensions.ExtensionPointName -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.NlsSafe -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow - -data class AgentPromptSuggestionRequest( - @JvmField val project: Project, - @JvmField val projectPath: String?, - @JvmField val targetModeId: String, - @JvmField val contextItems: List, -) - -enum class AgentPromptSuggestionProvenance { - TEMPLATE, - AI_POLISHED, - AI_GENERATED, -} - -data class AgentPromptSuggestionCandidate( - @JvmField val id: String, - @JvmField val label: @NlsSafe String, - @JvmField val promptText: @NlsSafe String, - @JvmField val provenance: AgentPromptSuggestionProvenance = AgentPromptSuggestionProvenance.TEMPLATE, -) - -data class AgentPromptSuggestionUpdate( - @JvmField val candidates: List, -) - -data class AgentPromptSuggestionAiRequest( - @JvmField val project: Project, - @JvmField val projectPath: String?, - @JvmField val targetModeId: String, - @JvmField val contextItems: List, - @JvmField val seedCandidates: List, - @JvmField val maxGeneratedCandidates: Int = DEFAULT_AI_GENERATED_SUGGESTION_CANDIDATES, -) - -data class AgentPromptSuggestionAiCandidate( - @JvmField val id: String? = null, - @JvmField val label: @NlsSafe String, - @JvmField val promptText: @NlsSafe String, -) - -sealed interface AgentPromptSuggestionAiResult { - data class PolishedSeeds( - @JvmField val candidates: List, - ) : AgentPromptSuggestionAiResult - - data class GeneratedCandidates( - @JvmField val candidates: List, - ) : AgentPromptSuggestionAiResult -} - -fun interface AgentPromptSuggestionGenerator { - fun generateSuggestions(request: AgentPromptSuggestionRequest): Flow -} - -fun interface AgentPromptSuggestionAiBackend { - suspend fun generateSuggestionResult(request: AgentPromptSuggestionAiRequest): AgentPromptSuggestionAiResult? -} - -private class AgentPromptSuggestionGeneratorRegistryLog - -private val LOG = logger() -private const val DEFAULT_AI_GENERATED_SUGGESTION_CANDIDATES = 3 -private const val MAX_AI_GENERATED_LABEL_LENGTH = 120 -private const val MAX_AI_GENERATED_PROMPT_TEXT_LENGTH = 1_000 -private val AGENT_PROMPT_SUGGESTION_AI_BACKEND_EP: ExtensionPointName = - ExtensionPointName("com.intellij.agent.workbench.promptSuggestionAiBackend") - -private val REGISTERED_PROMPT_SUGGESTION_AI_BACKEND = SingleExtensionPointResolver( - log = LOG, - extensionPoint = AGENT_PROMPT_SUGGESTION_AI_BACKEND_EP, - unavailableMessage = "Prompt suggestion AI backend EP is unavailable in this context", - multipleExtensionsMessage = { backends -> - "Multiple prompt suggestion AI backends registered; using first: ${backends.map { it::class.java.name }}" - }, -) - -private class DefaultAgentPromptSuggestionGenerator( - private val aiBackendProvider: () -> AgentPromptSuggestionAiBackend? = AgentPromptSuggestionAiBackends::find, -) : AgentPromptSuggestionGenerator { - override fun generateSuggestions(request: AgentPromptSuggestionRequest): Flow = flow { - val fallbackCandidates = AgentPromptSuggestionSeeds.buildDefaultSuggestions(request.contextItems) - emit(AgentPromptSuggestionUpdate(fallbackCandidates)) - - val aiBackend = aiBackendProvider() ?: return@flow - val aiRequest = request.toAgentPromptSuggestionAiRequest(fallbackCandidates) - val aiUpdate = try { - aiBackend.generateSuggestionResult(aiRequest)?.toSuggestionUpdate(aiRequest) - } - catch (t: Throwable) { - if (t is CancellationException) { - throw t - } - LOG.warn("Failed to load AI prompt suggestions", t) - null - } ?: return@flow - - emit(aiUpdate) - } -} - -object AgentPromptSuggestionAiBackends { - private val backendOverride = OverridableValue { REGISTERED_PROMPT_SUGGESTION_AI_BACKEND.findFirstOrNull() } - - fun find(): AgentPromptSuggestionAiBackend? { - return backendOverride.value() - } - - @Suppress("unused") - fun withBackendForTest(backend: AgentPromptSuggestionAiBackend?, action: () -> T): T { - return backendOverride.withOverride(backend, action) - } -} - -object AgentPromptSuggestionGenerators { - private val generatorOverride = OverridableValue { DefaultAgentPromptSuggestionGenerator() } - - fun find(): AgentPromptSuggestionGenerator? { - return generatorOverride.value() - } -} - -private fun AgentPromptSuggestionRequest.toAgentPromptSuggestionAiRequest( - fallbackCandidates: List, -): AgentPromptSuggestionAiRequest { - return AgentPromptSuggestionAiRequest( - project = project, - projectPath = projectPath, - targetModeId = targetModeId, - contextItems = contextItems, - seedCandidates = fallbackCandidates, - ) -} - -private fun AgentPromptSuggestionAiResult.toSuggestionUpdate(request: AgentPromptSuggestionAiRequest): AgentPromptSuggestionUpdate? { - return when (this) { - is AgentPromptSuggestionAiResult.GeneratedCandidates -> validateGeneratedCandidates( - request = request, - generatedCandidates = candidates, - )?.let(::AgentPromptSuggestionUpdate) - is AgentPromptSuggestionAiResult.PolishedSeeds -> validatePolishedCandidates( - fallbackCandidates = request.seedCandidates, - polishedCandidates = candidates, - )?.let(::AgentPromptSuggestionUpdate) - } -} - -private fun validatePolishedCandidates( - fallbackCandidates: List, - polishedCandidates: List, -): List? { - if (fallbackCandidates.isEmpty() || polishedCandidates.isEmpty()) { - LOG.debug("Ignoring polished AI prompt suggestions because there are no fallback seeds to polish") - return null - } - if (polishedCandidates.size != fallbackCandidates.size) { - LOG.debug("Ignoring polished AI prompt suggestions because the candidate count does not match fallback seeds") - return null - } - - val validated = ArrayList(fallbackCandidates.size) - fallbackCandidates.forEachIndexed { index, fallbackCandidate -> - val normalized = normalizeSuggestionCandidate(polishedCandidates[index]) - if (normalized?.id != fallbackCandidate.id) { - LOG.debug("Ignoring polished AI prompt suggestions because the slot ids do not match fallback seeds") - return null - } - validated += AgentPromptSuggestionCandidate( - id = fallbackCandidate.id, - label = normalized.label, - promptText = normalized.promptText, - provenance = AgentPromptSuggestionProvenance.AI_POLISHED, - ) - } - return validated -} - -private fun validateGeneratedCandidates( - request: AgentPromptSuggestionAiRequest, - generatedCandidates: List, -): List? { - if (generatedCandidates.isEmpty()) { - LOG.debug("Ignoring generated AI prompt suggestions because the backend returned no candidates") - return null - } - - val prefix = resolveGeneratedSuggestionPrefix(request.seedCandidates, request.contextItems) - val seenSuggestions = LinkedHashSet() - val maxCandidates = request.maxGeneratedCandidates.coerceAtLeast(1) - val validated = ArrayList(generatedCandidates.size.coerceAtMost(maxCandidates)) - generatedCandidates.forEachIndexed { index, generatedCandidate -> - if (validated.size >= maxCandidates) { - return@forEachIndexed - } - val normalized = normalizeSuggestionCandidate(generatedCandidate) ?: return@forEachIndexed - val dedupeKey = buildGeneratedSuggestionDedupeKey(normalized) - if (!seenSuggestions.add(dedupeKey)) { - return@forEachIndexed - } - validated += AgentPromptSuggestionCandidate( - id = buildGeneratedSuggestionId(prefix = prefix, label = normalized.label, index = index), - label = normalized.label, - promptText = normalized.promptText, - provenance = AgentPromptSuggestionProvenance.AI_GENERATED, - ) - } - if (validated.isEmpty()) { - LOG.debug("Ignoring AI prompt suggestions because all generated candidates were invalid or duplicates") - return null - } - return validated -} - -private fun normalizeSuggestionCandidate(candidate: AgentPromptSuggestionAiCandidate): AgentPromptSuggestionAiCandidate? { - val label = normalizeGeneratedLabel(candidate.label) ?: return null - val promptText = normalizeGeneratedPromptText(candidate.promptText) ?: return null - return AgentPromptSuggestionAiCandidate( - id = candidate.id?.trim()?.takeIf { it.isNotEmpty() }, - label = label, - promptText = promptText, - ) -} - -private fun normalizeGeneratedLabel(label: String): @NlsSafe String? { - return label - .lineSequence() - .map(String::trim) - .filter(String::isNotEmpty) - .joinToString(separator = " ") - .take(MAX_AI_GENERATED_LABEL_LENGTH) - .trim() - .takeIf { it.isNotEmpty() } -} - -private fun normalizeGeneratedPromptText(promptText: String): @NlsSafe String? { - return promptText - .trim() - .take(MAX_AI_GENERATED_PROMPT_TEXT_LENGTH) - .trim() - .takeIf { it.isNotEmpty() } -} - -private fun buildGeneratedSuggestionDedupeKey(candidate: AgentPromptSuggestionAiCandidate): String { - return candidate.label.lowercase() + '\u0000' + candidate.promptText.lowercase() -} - -private fun buildGeneratedSuggestionId(prefix: String, label: String, index: Int): String { - val slug = buildString { - label.lowercase().forEach { ch -> - when { - ch.isLetterOrDigit() -> append(ch) - ch == ' ' || ch == '-' || ch == '_' || ch == '/' || ch == '.' -> { - if (isNotEmpty() && last() != '-') { - append('-') - } - } - } - } - }.trim('-').ifEmpty { "candidate" } - return "$prefix.generated.$slug.${index + 1}" -} - -private fun resolveGeneratedSuggestionPrefix( - fallbackCandidates: List, - contextItems: List, -): String { - fallbackCandidates.firstOrNull()?.id - ?.substringBefore('.') - ?.takeIf { it.isNotBlank() } - ?.let { return it } - - return when { - contextItems.any { it.rendererId == AgentPromptContextRendererIds.TEST_FAILURES } -> "tests" - contextItems.any { it.rendererId == AgentPromptContextRendererIds.VCS_COMMITS } -> "vcs" - contextItems.any { - it.rendererId in setOf(AgentPromptContextRendererIds.SNIPPET, - AgentPromptContextRendererIds.FILE, - AgentPromptContextRendererIds.SYMBOL) - } -> "editor" - contextItems.any { it.rendererId == AgentPromptContextRendererIds.PATHS } -> "paths" - else -> "context" - } -} diff --git a/plugins/agent-workbench/prompt/core/src/AgentPromptSuggestionSeeds.kt b/plugins/agent-workbench/prompt/core/src/AgentPromptSuggestionSeeds.kt deleted file mode 100644 index f0dbb8193941..000000000000 --- a/plugins/agent-workbench/prompt/core/src/AgentPromptSuggestionSeeds.kt +++ /dev/null @@ -1,84 +0,0 @@ -// 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.prompt.core - -// @spec plugins/air/spec/actions/global-prompt-suggestions.spec.md - -object AgentPromptSuggestionSeeds { - fun buildDefaultSuggestions(contextItems: List): List { - val testsContext = contextItems.firstOrNull { it.rendererId == AgentPromptContextRendererIds.TEST_FAILURES } - if (testsContext != null) { - return if (extractFailedTestCount(testsContext) > 0) { - listOf( - suggestion("tests.fix"), - suggestion("tests.explain"), - suggestion("tests.bisect"), - ) - } - else { - listOf( - suggestion("tests.coverage"), - suggestion("tests.review"), - suggestion("tests.extend"), - ) - } - } - - if (contextItems.any { it.rendererId == AgentPromptContextRendererIds.VCS_COMMITS }) { - return listOf( - suggestion("vcs.review"), - suggestion("vcs.summary"), - suggestion("vcs.followup"), - ) - } - - if (contextItems.any(::isEditorContext)) { - return listOf( - suggestion("editor.explain"), - suggestion("editor.refactor"), - suggestion("editor.review"), - ) - } - - if (contextItems.any { it.rendererId == AgentPromptContextRendererIds.PATHS }) { - return listOf( - suggestion("paths.summary"), - suggestion("paths.impact"), - suggestion("paths.duplication"), - ) - } - - return emptyList() - } - - private fun isEditorContext(item: AgentPromptContextItem): Boolean { - return item.rendererId == AgentPromptContextRendererIds.SNIPPET || - item.rendererId == AgentPromptContextRendererIds.FILE || - item.rendererId == AgentPromptContextRendererIds.SYMBOL - } - - private fun extractFailedTestCount(item: AgentPromptContextItem): Int { - val payloadFields = item.payload.objOrNull()?.fields.orEmpty() - parseCount(payloadFields["statusCounts"]?.objOrNull()?.fields?.get("failed"))?.let { failedCount -> - return failedCount - } - return item.body.lineSequence().count { line -> - line.trimStart().startsWith("failed:", ignoreCase = true) - } - } - - private fun parseCount(value: AgentPromptPayloadValue?): Int? { - return when (value) { - is AgentPromptPayloadValue.Num -> value.value.toIntOrNull() - is AgentPromptPayloadValue.Str -> value.value.toIntOrNull() - else -> null - } - } - - private fun suggestion(id: String): AgentPromptSuggestionCandidate { - return AgentPromptSuggestionCandidate( - id = id, - label = AgentPromptSuggestionBundle.message("popup.suggestion.$id.label"), - promptText = AgentPromptSuggestionBundle.message("popup.suggestion.$id.prompt"), - ) - } -} diff --git a/plugins/agent-workbench/prompt/core/src/AgentPromptSuggestionSessionService.kt b/plugins/agent-workbench/prompt/core/src/AgentPromptSuggestionSessionService.kt deleted file mode 100644 index 127c80feb2ce..000000000000 --- a/plugins/agent-workbench/prompt/core/src/AgentPromptSuggestionSessionService.kt +++ /dev/null @@ -1,312 +0,0 @@ -// 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.prompt.core - -// @spec plugins/air/spec/actions/global-prompt-suggestions.spec.md - -import com.dynatrace.hash4j.hashing.HashSink -import com.dynatrace.hash4j.hashing.HashValue128 -import com.dynatrace.hash4j.hashing.Hashing -import com.intellij.openapi.components.Service -import com.intellij.openapi.diagnostic.logger -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch -import java.util.concurrent.atomic.AtomicBoolean -import kotlin.time.Duration.Companion.milliseconds - -private const val PROMPT_SUGGESTION_IDLE_RETENTION_MS = 30_000L - -private class AgentPromptSuggestionSessionServiceLog - -private val LOG = logger() - -@Service(Service.Level.PROJECT) -class AgentPromptSuggestionSessionService internal constructor( - private val serviceScope: CoroutineScope, - private val generatorProvider: () -> AgentPromptSuggestionGenerator?, - private val idleRetentionMs: Long, -) { - @Suppress("unused") - constructor(serviceScope: CoroutineScope) : this( - serviceScope = serviceScope, - generatorProvider = AgentPromptSuggestionGenerators::find, - idleRetentionMs = PROMPT_SUGGESTION_IDLE_RETENTION_MS, - ) - - private val lock = Any() - - private var retainedSession: RetainedPromptSuggestionSession? = null - - fun attach(request: AgentPromptSuggestionRequest): AgentPromptSuggestionSubscription? { - val requestKey = computePromptSuggestionRequestKey(request) - synchronized(lock) { - val existingSession = retainedSession - if (existingSession?.requestKey == requestKey) { - // Reuse same-fingerprint sessions while another attachment is still active, or after generation completed within the retention window. - existingSession.attachments += 1 - existingSession.evictionJob?.cancel() - existingSession.evictionJob = null - return PromptSuggestionSubscription(existingSession, ::detach) - } - - if (request.contextItems.isEmpty()) { - return null - } - - val generator = generatorProvider() ?: return null - val replacedSession = retainedSession - val newSession = createSession(requestKey, request, generator) - newSession.attachments = 1 - retainedSession = newSession - replacedSession?.cancel() - return PromptSuggestionSubscription(newSession, ::detach) - } - } - - private fun createSession( - requestKey: AgentPromptSuggestionRequestKey, - request: AgentPromptSuggestionRequest, - generator: AgentPromptSuggestionGenerator, - ): RetainedPromptSuggestionSession { - val candidates = MutableStateFlow>(emptyList()) - val generationJob = serviceScope.launch { - try { - generator.generateSuggestions(request).collect { update -> - candidates.value = update.candidates - } - } - catch (t: Throwable) { - if (t is CancellationException) { - throw t - } - LOG.warn("Failed to load prompt suggestions", t) - } - } - return RetainedPromptSuggestionSession( - requestKey = requestKey, - candidates = candidates, - generationJob = generationJob, - ) - } - - private fun detach(session: RetainedPromptSuggestionSession) { - var cancelAbandonedSession = false - synchronized(lock) { - if (retainedSession !== session) { - return - } - if (session.attachments > 0) { - session.attachments -= 1 - } - if (session.attachments != 0 || session.evictionJob != null) { - return - } - if (!session.generationJob.isCompleted) { - retainedSession = null - cancelAbandonedSession = true - } - else { - session.evictionJob = serviceScope.launch { - delay(idleRetentionMs.milliseconds) - evictIfIdle(session) - } - } - } - if (cancelAbandonedSession) { - session.cancel() - } - } - - private fun evictIfIdle(session: RetainedPromptSuggestionSession) { - synchronized(lock) { - if (retainedSession !== session || session.attachments != 0) { - return - } - retainedSession = null - session.evictionJob = null - session.cancel() - } - } -} - -interface AgentPromptSuggestionSubscription { - val currentCandidates: List - val updates: Flow> - - fun close() -} - -data class AgentPromptSuggestionRequestKey( - @JvmField val targetModeId: String, - @JvmField val projectPath: String?, - @JvmField val contextFingerprint: HashValue128?, -) - -fun computePromptSuggestionRequestKey(request: AgentPromptSuggestionRequest): AgentPromptSuggestionRequestKey { - return AgentPromptSuggestionRequestKey( - targetModeId = request.targetModeId, - projectPath = normalizeSuggestionProjectPath(request.projectPath), - contextFingerprint = computePromptSuggestionContextFingerprint(request.contextItems), - ) -} - -internal fun computePromptSuggestionContextFingerprint(items: List): HashValue128? { - if (items.isEmpty()) { - return null - } - - val hash = Hashing.xxh3_128().hashStream() - // version - hash.putInt(0) - - for (item in items) { - when (resolvePromptSuggestionContextHashMode(item)) { - PromptSuggestionContextHashMode.FULL -> appendExactContextFingerprintItem(hash, item) - PromptSuggestionContextHashMode.ROUNDED -> appendRoundedPromptSuggestionContextFingerprintItem(hash, item) - } - } - hash.putInt(items.size) - return hash.get() -} - -internal fun normalizeSuggestionProjectPath(projectPath: String?): String? { - return projectPath - ?.trim() - ?.takeIf { it.isNotEmpty() } - ?.let(::normalizeNonEmptySuggestionProjectPath) -} - -private fun normalizeNonEmptySuggestionProjectPath(projectPath: String): String { - return projectPath.replace('\\', '/').trimEnd('/') -} - -private enum class PromptSuggestionContextHashMode { - FULL, - ROUNDED, -} - -private fun resolvePromptSuggestionContextHashMode(item: AgentPromptContextItem): PromptSuggestionContextHashMode { - return if (item.isUnselectedEditorSnippetForPromptSuggestions()) { - PromptSuggestionContextHashMode.ROUNDED - } - else { - PromptSuggestionContextHashMode.FULL - } -} - -private fun appendRoundedPromptSuggestionContextFingerprintItem(sink: HashSink, item: AgentPromptContextItem) { - val payload = item.payload.objOrNull() - sink.putString(item.rendererId) - appendPromptSuggestionHashField(sink, "itemId", item.itemId) - appendPromptSuggestionHashField(sink, "parentItemId", item.parentItemId) - sink.putString(item.source) - sink.putInt(item.phase?.ordinal ?: -1) - sink.putBoolean(payload?.bool("selection") == true) - appendPromptSuggestionHashField(sink, "language", payload?.string("language")) -} - -private fun appendExactContextFingerprintItem(sink: HashSink, item: AgentPromptContextItem) { - sink.putString(item.rendererId) - appendPromptSuggestionHashField(sink, "title", item.title) - sink.putString(item.body) - appendPromptSuggestionHashField(sink, "itemId", item.itemId) - appendPromptSuggestionHashField(sink, "parentItemId", item.parentItemId) - sink.putString(item.source) - sink.putInt(item.phase?.ordinal ?: -1) - sink.putInt(item.truncation.originalChars) - sink.putInt(item.truncation.includedChars) - sink.putInt(item.truncation.reason.ordinal) - appendPromptSuggestionCanonicalPayload(sink, item.payload) -} - -private fun AgentPromptContextItem.isUnselectedEditorSnippetForPromptSuggestions(): Boolean { - if (rendererId != AgentPromptContextRendererIds.SNIPPET || source != "editor") { - return false - } - val payload = payload.objOrNull() ?: return false - return payload.bool("selection") == false -} - -private fun appendPromptSuggestionHashField(sink: HashSink, name: String, value: String?) { - sink.putString(name) - if (value == null) { - sink.putInt(-1) - } - else { - sink.putString(value) - } -} - -private fun appendPromptSuggestionCanonicalPayload(sink: HashSink, payload: AgentPromptPayloadValue) { - when (payload) { - is AgentPromptPayloadValue.Obj -> { - sink.putByte(0) - sink.putUnorderedIterable(payload.fields.keys, { key, nestedSink -> - nestedSink.putString(key) - appendPromptSuggestionCanonicalPayload(nestedSink, payload.fields.getValue(key)) - }, Hashing.xxh3_64()) - } - - is AgentPromptPayloadValue.Arr -> { - sink.putByte(1) - sink.putOrderedIterable(payload.items) { item, nestedSink -> - appendPromptSuggestionCanonicalPayload(nestedSink, item) - } - } - - is AgentPromptPayloadValue.Str -> { - sink.putByte(2) - sink.putString(payload.value) - } - - is AgentPromptPayloadValue.Num -> { - sink.putByte(3) - sink.putString(payload.value) - } - - is AgentPromptPayloadValue.Bool -> { - sink.putByte(4) - sink.putBoolean(payload.value) - } - - AgentPromptPayloadValue.Null -> sink.putByte(5) - } -} - -private class PromptSuggestionSubscription( - private val session: RetainedPromptSuggestionSession, - private val onClose: (RetainedPromptSuggestionSession) -> Unit, -) : AgentPromptSuggestionSubscription { - private val closed = AtomicBoolean(false) - - override val currentCandidates: List - get() = session.candidates.value - - override val updates: Flow> - get() = session.candidates - - override fun close() { - if (closed.compareAndSet(false, true)) { - onClose(session) - } - } -} - -private class RetainedPromptSuggestionSession( - val requestKey: AgentPromptSuggestionRequestKey, - val candidates: MutableStateFlow>, - val generationJob: Job, -) { - var attachments: Int = 0 - var evictionJob: Job? = null - - fun cancel() { - evictionJob?.cancel() - evictionJob = null - generationJob.cancel() - } -} diff --git a/plugins/agent-workbench/prompt/core/testSrc/AgentPromptContextEnvelopeFormatterTest.kt b/plugins/agent-workbench/prompt/core/testSrc/AgentPromptContextEnvelopeFormatterTest.kt deleted file mode 100644 index 1a2fac80cbd6..000000000000 --- a/plugins/agent-workbench/prompt/core/testSrc/AgentPromptContextEnvelopeFormatterTest.kt +++ /dev/null @@ -1,126 +0,0 @@ -// 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.prompt.core - -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.Timeout -import java.util.concurrent.TimeUnit - -@Timeout(value = 2, unit = TimeUnit.MINUTES) -class AgentPromptContextEnvelopeFormatterTest { - @Test - fun contextEnvelopePreparationKeepsEmptyContextBelowSoftCap() { - val selection = AgentPromptContextEnvelopeFormatter.prepareContextEnvelopeSelection( - items = emptyList(), - softCapChars = 1, - ) - - assertThat(selection.items).isEmpty() - assertThat(selection.serializedChars).isEqualTo(0) - assertThat(selection.exceedsSoftCap).isFalse() - } - - @Test - fun contextEnvelopePreparationNormalizesItemsAndReportsSoftCapState() { - val selection = AgentPromptContextEnvelopeFormatter.prepareContextEnvelopeSelection( - items = listOf(contextItem(rendererId = AgentPromptContextRendererIds.SNIPPET, title = "Selection", body = " selected code ")), - softCapChars = 1_000, - projectPath = "/work/repo", - ) - - assertThat(selection.items.single().body).isEqualTo("selected code") - assertThat(selection.summary.softCapChars).isEqualTo(1_000) - assertThat(selection.summary.softCapExceeded).isFalse() - assertThat(selection.summary.autoTrimApplied).isFalse() - assertThat(selection.exceedsSoftCap).isFalse() - } - - @Test - fun contextEnvelopePreparationCanMarkFullOversizedSelectionWithoutTrimming() { - val oversizedBody = "x".repeat(1_000) - val selection = AgentPromptContextEnvelopeFormatter.prepareContextEnvelopeSelection( - items = listOf(contextItem(rendererId = AgentPromptContextRendererIds.SNIPPET, title = "Selection", body = oversizedBody)), - softCapChars = 400, - projectPath = "/work/repo", - ) - - val marked = AgentPromptContextEnvelopeFormatter.markSoftCapExceeded(selection) - - assertThat(marked.items.single().body).isEqualTo(oversizedBody) - assertThat(marked.summary.softCapExceeded).isTrue() - assertThat(marked.summary.autoTrimApplied).isFalse() - assertThat(marked.exceedsSoftCap).isTrue() - } - - @Test - fun contextEnvelopePreparationAutoTrimsOversizedSelection() { - val oversizedBody = "x".repeat(AgentPromptContextEnvelopeFormatter.DEFAULT_SOFT_CAP_CHARS * 2) - val selection = AgentPromptContextEnvelopeFormatter.prepareContextEnvelopeSelection( - items = listOf(contextItem(rendererId = AgentPromptContextRendererIds.SNIPPET, title = "Selection", body = oversizedBody)), - projectPath = "/work/repo", - ) - - val trimmed = AgentPromptContextEnvelopeFormatter.autoTrimContextEnvelopeSelection( - selection = selection, - projectPath = "/work/repo", - ) - - assertThat(trimmed.summary.softCapExceeded).isTrue() - assertThat(trimmed.summary.autoTrimApplied).isTrue() - assertThat(trimmed.items).isNotEmpty() - assertThat(trimmed.items.joinToString("\n") { item -> item.body }).isNotEqualTo(oversizedBody) - assertThat(trimmed.serializedChars).isLessThanOrEqualTo(AgentPromptContextEnvelopeFormatter.DEFAULT_SOFT_CAP_CHARS) - assertThat(trimmed.exceedsSoftCap).isFalse() - } - - @Test - fun softCapTrimmingStartsFromLastContextItem() { - val first = contextItem(rendererId = AgentPromptContextRendererIds.SNIPPET, title = "First", body = "a".repeat(420)) - val second = contextItem(rendererId = AgentPromptContextRendererIds.PATHS, title = "Second", body = "b".repeat(420)) - - val result = AgentPromptContextEnvelopeFormatter.applySoftCap( - items = listOf(first, second), - softCapChars = 700, - ) - - assertThat(result.items).hasSize(2) - assertThat(result.items.first().body).isEqualTo(first.body) - assertThat(result.items.last().truncation.reason).isIn( - AgentPromptContextTruncationReason.SOFT_CAP_PARTIAL, - AgentPromptContextTruncationReason.SOFT_CAP_OMITTED, - ) - assertThat(result.serializedChars).isLessThanOrEqualTo(700) - assertThat(result.exceedsSoftCap).isFalse() - } - - @Test - fun verySmallSoftCapKeepsItemStubsInsteadOfDroppingItems() { - val result = AgentPromptContextEnvelopeFormatter.applySoftCap( - items = listOf( - contextItem(rendererId = AgentPromptContextRendererIds.SNIPPET, title = "A", body = "x".repeat(200)), - contextItem(rendererId = AgentPromptContextRendererIds.PATHS, title = "B", body = "y".repeat(200)), - ), - softCapChars = 200, - ) - - assertThat(result.items).hasSize(2) - assertThat(result.items.count { item -> item.body == "[omitted due to soft cap]" }).isGreaterThan(0) - assertThat(result.items) - .allSatisfy { item -> - assertThat(item.truncation.reason).isNotEqualTo(AgentPromptContextTruncationReason.NONE) - } - } - - private fun contextItem( - rendererId: String, - title: String, - body: String, - ): AgentPromptContextItem { - return AgentPromptContextItem( - rendererId = rendererId, - title = title, - body = body, - source = "test", - ) - } -} diff --git a/plugins/agent-workbench/prompt/core/testSrc/AgentPromptContextResolverServiceTest.kt b/plugins/agent-workbench/prompt/core/testSrc/AgentPromptContextResolverServiceTest.kt deleted file mode 100644 index 5db80881afff..000000000000 --- a/plugins/agent-workbench/prompt/core/testSrc/AgentPromptContextResolverServiceTest.kt +++ /dev/null @@ -1,183 +0,0 @@ -// 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.prompt.core - -import com.intellij.openapi.actionSystem.CommonDataKeys -import com.intellij.openapi.actionSystem.impl.SimpleDataContext -import com.intellij.openapi.project.ProjectManager -import com.intellij.testFramework.junit5.TestApplication -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.Timeout -import java.util.concurrent.TimeUnit - -@TestApplication -@Timeout(value = 2, unit = TimeUnit.MINUTES) -class AgentPromptContextResolverServiceTest { - @Test - fun invocationPhaseUsesFirstNonEmptyContributor() { - var firstInvocationCalls = 0 - var secondInvocationCalls = 0 - var fallbackCalls = 0 - - val service = AgentPromptContextResolverService( - contributorsProvider = { - listOf( - testContributor(contributorPhase = AgentPromptContextContributorPhase.INVOCATION) { - firstInvocationCalls++ - emptyList() - }, - testContributor(contributorPhase = AgentPromptContextContributorPhase.INVOCATION) { - secondInvocationCalls++ - listOf(contextItem(AgentPromptContextRendererIds.SNIPPET)) - }, - testContributor(contributorPhase = AgentPromptContextContributorPhase.FALLBACK) { - fallbackCalls++ - listOf(contextItem(AgentPromptContextRendererIds.PATHS)) - }, - ) - } - ) - - val resolved = service.collectDefaultContext(invocationData()) - - assertThat(resolved.map { it.rendererId }).containsExactly(AgentPromptContextRendererIds.SNIPPET) - assertThat(resolved.single().phase).isEqualTo(AgentPromptContextContributorPhase.INVOCATION) - assertThat(firstInvocationCalls).isEqualTo(1) - assertThat(secondInvocationCalls).isEqualTo(1) - assertThat(fallbackCalls).isZero() - } - - @Test - fun fallbackPhaseIsUsedWhenInvocationContributorsAreEmpty() { - var fallbackCalls = 0 - - val service = AgentPromptContextResolverService( - contributorsProvider = { - listOf( - testContributor(contributorPhase = AgentPromptContextContributorPhase.INVOCATION) { emptyList() }, - testContributor(contributorPhase = AgentPromptContextContributorPhase.FALLBACK) { - fallbackCalls++ - listOf(contextItem(AgentPromptContextRendererIds.PATHS)) - }, - ) - } - ) - - val resolved = service.collectDefaultContext(invocationData()) - - assertThat(resolved.map { it.rendererId }).containsExactly(AgentPromptContextRendererIds.PATHS) - assertThat(resolved.single().phase).isEqualTo(AgentPromptContextContributorPhase.FALLBACK) - assertThat(fallbackCalls).isEqualTo(1) - } - - @Test - fun failingContributorIsIgnoredAndResolutionContinues() { - val service = AgentPromptContextResolverService( - contributorsProvider = { - listOf( - testContributor(contributorPhase = AgentPromptContextContributorPhase.INVOCATION) { - error("boom") - }, - testContributor(contributorPhase = AgentPromptContextContributorPhase.INVOCATION) { - listOf(contextItem(AgentPromptContextRendererIds.FILE)) - }, - ) - } - ) - - val resolved = service.collectDefaultContext(invocationData()) - - assertThat(resolved.map { it.rendererId }).containsExactly(AgentPromptContextRendererIds.FILE) - assertThat(resolved.single().phase).isEqualTo(AgentPromptContextContributorPhase.INVOCATION) - } - - @Test - fun registrationOrderWinsOverClassNameSortingWithinPhase() { - val service = AgentPromptContextResolverService( - contributorsProvider = { - listOf( - ZuluInvocationContributor(contextItem(AgentPromptContextRendererIds.FILE)), - AlphaInvocationContributor(contextItem(AgentPromptContextRendererIds.SNIPPET)), - ) - } - ) - - val resolved = service.collectDefaultContext(invocationData()) - - assertThat(resolved.map { it.rendererId }).containsExactly(AgentPromptContextRendererIds.FILE) - } - - @Test - fun noImplicitContextPolicySkipsContributors() { - var calls = 0 - val service = AgentPromptContextResolverService( - contributorsProvider = { - listOf(testContributor(contributorPhase = AgentPromptContextContributorPhase.INVOCATION) { - calls++ - listOf(contextItem(AgentPromptContextRendererIds.FILE)) - }) - } - ) - - val resolved = service.collectDefaultContext(invocationData(defaultContextPolicy = AgentPromptDefaultContextPolicy.NO_IMPLICIT_CONTEXT)) - - assertThat(resolved).isEmpty() - assertThat(calls).isZero() - } - - private fun invocationData( - dataContext: com.intellij.openapi.actionSystem.DataContext? = null, - defaultContextPolicy: AgentPromptDefaultContextPolicy = AgentPromptDefaultContextPolicy.ALLOW_IMPLICIT_CONTEXT, - ): AgentPromptInvocationData { - val project = ProjectManager.getInstance().defaultProject - val effectiveDataContext = dataContext ?: SimpleDataContext.builder() - .add(CommonDataKeys.PROJECT, project) - .build() - return AgentPromptInvocationData( - project = project, - actionId = "AgentWorkbenchPrompt.OpenGlobalPalette", - actionText = "Ask Agent", - actionPlace = "MainMenu", - invokedAtMs = 0L, - defaultContextPolicy = defaultContextPolicy, - attributes = mapOf( - AGENT_PROMPT_INVOCATION_DATA_CONTEXT_KEY to effectiveDataContext, - ), - ) - } - - private fun contextItem(rendererId: String, body: String = "Value"): AgentPromptContextItem { - return AgentPromptContextItem( - rendererId = rendererId, - title = "Context", - body = body, - source = "test", - ) - } - - private fun testContributor( - contributorPhase: AgentPromptContextContributorPhase, - collector: (AgentPromptInvocationData) -> List, - ): AgentPromptContextContributorBridge { - return object : AgentPromptContextContributorBridge { - override val phase: AgentPromptContextContributorPhase - get() = contributorPhase - - override fun collect(invocationData: AgentPromptInvocationData): List { - return collector(invocationData) - } - } - } - - private class AlphaInvocationContributor( - private val item: AgentPromptContextItem, - ) : AgentPromptContextContributorBridge { - override fun collect(invocationData: AgentPromptInvocationData): List = listOf(item) - } - - private class ZuluInvocationContributor( - private val item: AgentPromptContextItem, - ) : AgentPromptContextContributorBridge { - override fun collect(invocationData: AgentPromptInvocationData): List = listOf(item) - } -} diff --git a/plugins/agent-workbench/prompt/core/testSrc/AgentPromptDefaultSuggestionGeneratorTest.kt b/plugins/agent-workbench/prompt/core/testSrc/AgentPromptDefaultSuggestionGeneratorTest.kt deleted file mode 100644 index b901c60beb0d..000000000000 --- a/plugins/agent-workbench/prompt/core/testSrc/AgentPromptDefaultSuggestionGeneratorTest.kt +++ /dev/null @@ -1,112 +0,0 @@ -// 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.prompt.core - -import com.intellij.testFramework.junit5.TestApplication -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.Timeout -import java.util.concurrent.TimeUnit - -@TestApplication -@Timeout(value = 2, unit = TimeUnit.MINUTES) -class AgentPromptSuggestionSeedsTest { - @Test - fun failingTestsProduceFixFirstSuggestions() { - val suggestions = AgentPromptSuggestionSeeds.buildDefaultSuggestions( - listOf( - contextItem( - rendererId = AgentPromptContextRendererIds.TEST_FAILURES, - payload = AgentPromptPayload.obj( - "statusCounts" to AgentPromptPayload.obj( - "failed" to AgentPromptPayload.num(2), - "passed" to AgentPromptPayload.num(1), - ) - ) - ) - ) - ) - - assertThat(suggestions.map(AgentPromptSuggestionCandidate::id)) - .containsExactly("tests.fix", "tests.explain", "tests.bisect") - } - - @Test - fun passingTestsProduceCoverageSuggestions() { - val suggestions = AgentPromptSuggestionSeeds.buildDefaultSuggestions( - listOf( - contextItem( - rendererId = AgentPromptContextRendererIds.TEST_FAILURES, - payload = AgentPromptPayload.obj( - "statusCounts" to AgentPromptPayload.obj( - "passed" to AgentPromptPayload.num(1), - "ignored" to AgentPromptPayload.num(1), - ) - ) - ) - ) - ) - - assertThat(suggestions.map(AgentPromptSuggestionCandidate::id)) - .containsExactly("tests.coverage", "tests.review", "tests.extend") - } - - @Test - fun vcsContextProducesCommitSuggestions() { - val suggestions = AgentPromptSuggestionSeeds.buildDefaultSuggestions( - listOf(contextItem(rendererId = AgentPromptContextRendererIds.VCS_COMMITS)) - ) - - assertThat(suggestions.map(AgentPromptSuggestionCandidate::id)) - .containsExactly("vcs.review", "vcs.summary", "vcs.followup") - } - - @Test - fun editorContextProducesCodeSuggestions() { - val suggestions = AgentPromptSuggestionSeeds.buildDefaultSuggestions( - listOf(contextItem(rendererId = AgentPromptContextRendererIds.SNIPPET)) - ) - - assertThat(suggestions.map(AgentPromptSuggestionCandidate::id)) - .containsExactly("editor.explain", "editor.refactor", "editor.review") - } - - @Test - fun pathsContextProducesFileSuggestions() { - val suggestions = AgentPromptSuggestionSeeds.buildDefaultSuggestions( - listOf(contextItem(rendererId = AgentPromptContextRendererIds.PATHS)) - ) - - assertThat(suggestions.map(AgentPromptSuggestionCandidate::id)) - .containsExactly("paths.summary", "paths.impact", "paths.duplication") - } - - @Test - fun testContextOutranksVcsContext() { - val suggestions = AgentPromptSuggestionSeeds.buildDefaultSuggestions( - listOf( - contextItem(rendererId = AgentPromptContextRendererIds.VCS_COMMITS), - contextItem( - rendererId = AgentPromptContextRendererIds.TEST_FAILURES, - payload = AgentPromptPayload.obj( - "statusCounts" to AgentPromptPayload.obj("failed" to AgentPromptPayload.num(1)) - ) - ), - ) - ) - - assertThat(suggestions.map(AgentPromptSuggestionCandidate::id)) - .containsExactly("tests.fix", "tests.explain", "tests.bisect") - } - - private fun contextItem( - rendererId: String, - payload: com.intellij.agent.workbench.prompt.core.AgentPromptPayloadValue = AgentPromptPayload.obj(), - ): AgentPromptContextItem { - return AgentPromptContextItem( - rendererId = rendererId, - title = rendererId, - body = "body", - payload = payload, - ) - } -} diff --git a/plugins/agent-workbench/prompt/core/testSrc/AgentPromptSuggestionGeneratorTest.kt b/plugins/agent-workbench/prompt/core/testSrc/AgentPromptSuggestionGeneratorTest.kt deleted file mode 100644 index c845f6b57343..000000000000 --- a/plugins/agent-workbench/prompt/core/testSrc/AgentPromptSuggestionGeneratorTest.kt +++ /dev/null @@ -1,194 +0,0 @@ -// 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.prompt.core - -import com.intellij.openapi.project.ProjectManager -import com.intellij.testFramework.junit5.TestApplication -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.runBlocking -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.Timeout -import java.util.concurrent.TimeUnit - -@TestApplication -@Timeout(value = 2, unit = TimeUnit.MINUTES) -class AgentPromptSuggestionGeneratorTest { - @Test - fun defaultGeneratorEmitsFallbackSuggestionsBeforePolishedAiUpdate() { - val updates = AgentPromptSuggestionAiBackends.withBackendForTest( - AgentPromptSuggestionAiBackend { request -> - AgentPromptSuggestionAiResult.PolishedSeeds( - request.seedCandidates.map { candidate -> - AgentPromptSuggestionAiCandidate( - id = candidate.id, - label = "AI ${candidate.label}", - promptText = "${candidate.promptText} Keep the answer concise.", - ) - } - ) - } - ) { - runBlocking(Dispatchers.Default) { - checkNotNull(AgentPromptSuggestionGenerators.find()) - .generateSuggestions(failedTestRequest()) - .toList() - } - } - - assertThat(updates).hasSize(2) - assertThat(updates.first().candidates.map(AgentPromptSuggestionCandidate::id)) - .containsExactly("tests.fix", "tests.explain", "tests.bisect") - assertThat(updates.first().candidates.map(AgentPromptSuggestionCandidate::provenance)) - .containsOnly(AgentPromptSuggestionProvenance.TEMPLATE) - assertThat(updates.last().candidates.map(AgentPromptSuggestionCandidate::id)) - .containsExactly("tests.fix", "tests.explain", "tests.bisect") - assertThat(updates.last().candidates.map(AgentPromptSuggestionCandidate::provenance)) - .containsOnly(AgentPromptSuggestionProvenance.AI_POLISHED) - } - - @Test - fun defaultGeneratorAllowsAiGeneratedSuggestionsWithoutFallbackSeeds() { - val updates = AgentPromptSuggestionAiBackends.withBackendForTest( - AgentPromptSuggestionAiBackend { - AgentPromptSuggestionAiResult.GeneratedCandidates( - listOf( - AgentPromptSuggestionAiCandidate( - label = "Summarize the custom context", - promptText = "Summarize the custom context and highlight what needs attention.", - ) - ) - ) - } - ) { - runBlocking(Dispatchers.Default) { - checkNotNull(AgentPromptSuggestionGenerators.find()) - .generateSuggestions(customContextRequest()) - .toList() - } - } - - assertThat(updates).hasSize(2) - assertThat(updates.first().candidates).isEmpty() - assertThat(updates.last().candidates.single().provenance).isEqualTo(AgentPromptSuggestionProvenance.AI_GENERATED) - assertThat(updates.last().candidates.single().id).isEqualTo("context.generated.summarize-the-custom-context.1") - } - - @Test - fun defaultGeneratorKeepsFallbackWhenPolishedCandidatesDoNotMatchFallbackSlots() { - val updates = AgentPromptSuggestionAiBackends.withBackendForTest( - AgentPromptSuggestionAiBackend { - AgentPromptSuggestionAiResult.PolishedSeeds( - listOf( - AgentPromptSuggestionAiCandidate( - id = "tests.fix", - label = "AI: Fix the ParserTest failure", - promptText = "Investigate ParserTest, identify the root cause, and implement the minimal fix.", - ), - AgentPromptSuggestionAiCandidate( - id = "tests.other", - label = "AI: Explain the ParserTest failure", - promptText = "Explain why ParserTest is failing and point out the relevant code path.", - ), - AgentPromptSuggestionAiCandidate( - id = "tests.bisect", - label = "AI: Bisect the ParserTest regression", - promptText = "Identify the commit that broke ParserTest by reading recent diffs of the test and its production paths.", - ), - ) - ) - } - ) { - runBlocking(Dispatchers.Default) { - checkNotNull(AgentPromptSuggestionGenerators.find()) - .generateSuggestions(failedTestRequest()) - .toList() - } - } - - assertThat(updates).hasSize(1) - assertThat(updates.single().candidates.map(AgentPromptSuggestionCandidate::provenance)) - .containsOnly(AgentPromptSuggestionProvenance.TEMPLATE) - } - - @Test - fun defaultGeneratorFiltersInvalidAndDuplicateGeneratedCandidatesBeforeReplacingFallback() { - val updates = AgentPromptSuggestionAiBackends.withBackendForTest( - AgentPromptSuggestionAiBackend { - AgentPromptSuggestionAiResult.GeneratedCandidates( - listOf( - AgentPromptSuggestionAiCandidate(label = " ", promptText = "ignored"), - AgentPromptSuggestionAiCandidate(label = "Explain the failure", promptText = "Explain the failing test and likely root cause."), - AgentPromptSuggestionAiCandidate(label = "Explain the failure", promptText = "Explain the failing test and likely root cause."), - AgentPromptSuggestionAiCandidate(label = "Draft a fix plan", promptText = "Draft a concrete plan to fix the failing test."), - ) - ) - } - ) { - runBlocking(Dispatchers.Default) { - checkNotNull(AgentPromptSuggestionGenerators.find()) - .generateSuggestions(failedTestRequest()) - .toList() - } - } - - assertThat(updates).hasSize(2) - assertThat(updates.last().candidates.map(AgentPromptSuggestionCandidate::label)) - .containsExactly("Explain the failure", "Draft a fix plan") - assertThat(updates.last().candidates.map(AgentPromptSuggestionCandidate::provenance)) - .containsOnly(AgentPromptSuggestionProvenance.AI_GENERATED) - } - - @Test - fun defaultGeneratorFallsBackToTemplatesWhenNoAiBackendIsAvailable() { - val updates = AgentPromptSuggestionAiBackends.withBackendForTest(null) { - runBlocking(Dispatchers.Default) { - checkNotNull(AgentPromptSuggestionGenerators.find()) - .generateSuggestions(failedTestRequest()) - .toList() - } - } - - assertThat(updates).hasSize(1) - assertThat(updates.single().candidates.map(AgentPromptSuggestionCandidate::id)) - .containsExactly("tests.fix", "tests.explain", "tests.bisect") - assertThat(updates.single().candidates.map(AgentPromptSuggestionCandidate::provenance)) - .containsOnly(AgentPromptSuggestionProvenance.TEMPLATE) - } - - private fun failedTestRequest(): AgentPromptSuggestionRequest { - return AgentPromptSuggestionRequest( - project = ProjectManager.getInstance().defaultProject, - projectPath = "/work/project/", - targetModeId = "NEW_TASK", - contextItems = listOf( - AgentPromptContextItem( - rendererId = AgentPromptContextRendererIds.TEST_FAILURES, - title = "Failing tests", - body = "failed: ParserTest", - payload = AgentPromptPayload.obj( - "statusCounts" to AgentPromptPayload.obj( - "failed" to AgentPromptPayload.num(2), - ) - ), - source = "testRunner", - ) - ), - ) - } - - private fun customContextRequest(): AgentPromptSuggestionRequest { - return AgentPromptSuggestionRequest( - project = ProjectManager.getInstance().defaultProject, - projectPath = "/work/project/", - targetModeId = "NEW_TASK", - contextItems = listOf( - AgentPromptContextItem( - rendererId = "customContext", - title = "Custom context", - body = "A proprietary context block that the fallback heuristics do not understand.", - ) - ), - ) - } -} diff --git a/plugins/agent-workbench/prompt/core/testSrc/AgentPromptSuggestionSessionServiceTest.kt b/plugins/agent-workbench/prompt/core/testSrc/AgentPromptSuggestionSessionServiceTest.kt deleted file mode 100644 index ab54497abc1f..000000000000 --- a/plugins/agent-workbench/prompt/core/testSrc/AgentPromptSuggestionSessionServiceTest.kt +++ /dev/null @@ -1,550 +0,0 @@ -// 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.prompt.core - -import com.intellij.openapi.project.ProjectManager -import com.intellij.testFramework.junit5.TestApplication -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.Timeout -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicInteger - -@TestApplication -@Timeout(value = 2, unit = TimeUnit.MINUTES) -class AgentPromptSuggestionSessionServiceTest { - @Test - fun roundedUnselectedEditorContextReusesSameRetainedSession() { - val scope = serviceScope() - val generatorCallCount = AtomicInteger(0) - try { - val service = AgentPromptSuggestionSessionService( - serviceScope = scope, - generatorProvider = { - AgentPromptSuggestionGenerator { - val call = generatorCallCount.incrementAndGet() - flowOf(AgentPromptSuggestionUpdate(listOf(candidate(id = "call-$call")))) - } - }, - idleRetentionMs = 50L, - ) - - val firstSubscription = checkNotNull( - service.attach( - request( - label = "first", - contextItems = editorContextItems( - symbolName = "main", - selection = false, - snippetTitle = "Lines 10-16", - snippetBody = "val answer = 42", - startLine = 10, - endLine = 16, - ), - ) - ) - ) - waitForCondition { firstSubscription.currentCandidates.singleOrNull()?.id == "call-1" } - firstSubscription.close() - - Thread.sleep(20) - - val secondSubscription = checkNotNull( - service.attach( - request( - label = "first", - contextItems = editorContextItems( - symbolName = "main", - selection = false, - snippetTitle = "Lines 40-46", - snippetBody = "return helper()", - startLine = 40, - endLine = 46, - truncation = AgentPromptContextTruncation( - originalChars = 500, - includedChars = 120, - reason = AgentPromptContextTruncationReason.SOURCE_LIMIT, - ), - ), - ) - ) - ) - - assertThat(generatorCallCount.get()).isEqualTo(1) - assertThat(secondSubscription.currentCandidates.single().id).isEqualTo("call-1") - - secondSubscription.close() - } - finally { - scope.cancel() - } - } - - @Test - fun closingLastSubscriptionCancelsInFlightGenerationImmediately() { - val scope = serviceScope() - val firstCancelled = CompletableDeferred() - val generatorCallCount = AtomicInteger(0) - try { - val service = AgentPromptSuggestionSessionService( - serviceScope = scope, - generatorProvider = { - AgentPromptSuggestionGenerator { - val call = generatorCallCount.incrementAndGet() - flow { - emit(AgentPromptSuggestionUpdate(listOf(candidate(id = "call-$call")))) - try { - awaitCancellation() - } - finally { - if (call == 1) { - firstCancelled.complete(Unit) - } - } - } - } - }, - idleRetentionMs = 5_000L, - ) - - val firstSubscription = checkNotNull(service.attach(request(label = "first"))) - waitForCondition { firstSubscription.currentCandidates.map(AgentPromptSuggestionCandidate::id) == listOf("call-1") } - firstSubscription.close() - - waitForCondition(timeoutMs = 500) { firstCancelled.isCompleted } - val secondSubscription = checkNotNull(service.attach(request(label = "first"))) - waitForCondition { secondSubscription.currentCandidates.map(AgentPromptSuggestionCandidate::id) == listOf("call-2") } - - assertThat(generatorCallCount.get()).isEqualTo(2) - assertThat(secondSubscription.currentCandidates.map(AgentPromptSuggestionCandidate::id)).containsExactly("call-2") - - secondSubscription.close() - } - finally { - scope.cancel() - } - } - - @Test - fun completedRequestIsReusedWithinRetentionWindow() { - val scope = serviceScope() - val generatorCallCount = AtomicInteger(0) - try { - val service = AgentPromptSuggestionSessionService( - serviceScope = scope, - generatorProvider = { - AgentPromptSuggestionGenerator { - generatorCallCount.incrementAndGet() - flowOf(AgentPromptSuggestionUpdate(listOf(candidate(id = "tests.fix", provenance = AgentPromptSuggestionProvenance.AI_POLISHED)))) - } - }, - idleRetentionMs = 50L, - ) - - val firstSubscription = checkNotNull(service.attach(request(label = "first"))) - waitForCondition { firstSubscription.currentCandidates.map(AgentPromptSuggestionCandidate::id) == listOf("tests.fix") } - firstSubscription.close() - - Thread.sleep(20) - val secondSubscription = checkNotNull(service.attach(request(label = "first"))) - - assertThat(generatorCallCount.get()).isEqualTo(1) - assertThat(secondSubscription.currentCandidates.single().provenance).isEqualTo(AgentPromptSuggestionProvenance.AI_POLISHED) - - secondSubscription.close() - } - finally { - scope.cancel() - } - } - - @Test - fun requestIsEvictedAfterIdleTimeout() { - val scope = serviceScope() - val generatorCallCount = AtomicInteger(0) - try { - val service = AgentPromptSuggestionSessionService( - serviceScope = scope, - generatorProvider = { - AgentPromptSuggestionGenerator { - val call = generatorCallCount.incrementAndGet() - flowOf(AgentPromptSuggestionUpdate(listOf(candidate(id = "call-$call")))) - } - }, - idleRetentionMs = 50L, - ) - - val firstSubscription = checkNotNull(service.attach(request(label = "first"))) - waitForCondition { firstSubscription.currentCandidates.singleOrNull()?.id == "call-1" } - firstSubscription.close() - - Thread.sleep(120) - - val secondSubscription = checkNotNull(service.attach(request(label = "first"))) - waitForCondition { secondSubscription.currentCandidates.singleOrNull()?.id == "call-2" } - - assertThat(generatorCallCount.get()).isEqualTo(2) - - secondSubscription.close() - } - finally { - scope.cancel() - } - } - - @Test - fun differentRequestReplacesRetainedSessionImmediately() { - val scope = serviceScope() - val firstStarted = CompletableDeferred() - val firstCancelled = CompletableDeferred() - val generatorCallCount = AtomicInteger(0) - try { - val service = AgentPromptSuggestionSessionService( - serviceScope = scope, - generatorProvider = { - AgentPromptSuggestionGenerator { request -> - generatorCallCount.incrementAndGet() - flow { - if (request.contextItems.single().title == "first") { - firstStarted.complete(Unit) - try { - awaitCancellation() - } - finally { - firstCancelled.complete(Unit) - } - } - else { - emit(AgentPromptSuggestionUpdate(listOf(candidate(id = "fresh")))) - } - } - } - }, - idleRetentionMs = 50L, - ) - - val firstSubscription = checkNotNull(service.attach(request(label = "first"))) - waitForCondition { firstStarted.isCompleted } - - val secondSubscription = checkNotNull(service.attach(request(label = "second"))) - waitForCondition { firstCancelled.isCompleted } - waitForCondition { secondSubscription.currentCandidates.map(AgentPromptSuggestionCandidate::id) == listOf("fresh") } - - assertThat(generatorCallCount.get()).isEqualTo(2) - - firstSubscription.close() - secondSubscription.close() - } - finally { - scope.cancel() - } - } - - @Test - fun reopeningOriginalContextAfterIntermediateSwitchStartsOnlyFreshGenerations() { - val scope = serviceScope() - val startedLabels = mutableListOf() - val cancelledLabels = mutableListOf() - val generatorCallCount = AtomicInteger(0) - try { - val service = AgentPromptSuggestionSessionService( - serviceScope = scope, - generatorProvider = { - AgentPromptSuggestionGenerator { request -> - val label = request.contextItems.single().title ?: "missing" - val call = generatorCallCount.incrementAndGet() - flow { - synchronized(startedLabels) { - startedLabels += label - } - emit(AgentPromptSuggestionUpdate(listOf(candidate(id = "call-$call")))) - try { - awaitCancellation() - } - finally { - synchronized(cancelledLabels) { - cancelledLabels += label - } - } - } - } - }, - idleRetentionMs = 50L, - ) - - val firstSubscription = checkNotNull(service.attach(request(label = "first"))) - waitForCondition { firstSubscription.currentCandidates.map(AgentPromptSuggestionCandidate::id) == listOf("call-1") } - firstSubscription.close() - waitForCondition(timeoutMs = 500) { synchronized(cancelledLabels) { cancelledLabels.count { it == "first" } } == 1 } - - val secondSubscription = checkNotNull(service.attach(request(label = "second"))) - waitForCondition { secondSubscription.currentCandidates.map(AgentPromptSuggestionCandidate::id) == listOf("call-2") } - secondSubscription.close() - waitForCondition(timeoutMs = 500) { synchronized(cancelledLabels) { cancelledLabels.count { it == "second" } } == 1 } - - val thirdSubscription = checkNotNull(service.attach(request(label = "first"))) - waitForCondition { thirdSubscription.currentCandidates.map(AgentPromptSuggestionCandidate::id) == listOf("call-3") } - - assertThat(generatorCallCount.get()).isEqualTo(3) - assertThat(startedLabels).containsExactly("first", "second", "first") - - thirdSubscription.close() - } - finally { - scope.cancel() - } - } - - @Test - fun roundedUnselectedEditorSnippetKeyIgnoresCaretWindowDetails() { - val first = computePromptSuggestionRequestKey( - request( - label = "first", - contextItems = editorContextItems( - symbolName = "main", - selection = false, - snippetTitle = "Lines 10-16", - snippetBody = "val answer = 42", - startLine = 10, - endLine = 16, - ), - ) - ) - val second = computePromptSuggestionRequestKey( - request( - label = "first", - contextItems = editorContextItems( - symbolName = "main", - selection = false, - snippetTitle = "Lines 40-46", - snippetBody = "return helper()", - startLine = 40, - endLine = 46, - truncation = AgentPromptContextTruncation( - originalChars = 500, - includedChars = 120, - reason = AgentPromptContextTruncationReason.SOURCE_LIMIT, - ), - ), - ) - ) - - assertThat(first).isEqualTo(second) - } - - @Test - fun editorSymbolChangeChangesRoundedSuggestionKey() { - val first = computePromptSuggestionRequestKey( - request( - label = "first", - contextItems = editorContextItems( - symbolName = "main", - selection = false, - snippetTitle = "Lines 10-16", - snippetBody = "val answer = 42", - startLine = 10, - endLine = 16, - ), - ) - ) - val second = computePromptSuggestionRequestKey( - request( - label = "first", - contextItems = editorContextItems( - symbolName = "helper", - selection = false, - snippetTitle = "Lines 40-46", - snippetBody = "return helper()", - startLine = 40, - endLine = 46, - ), - ) - ) - - assertThat(first).isNotEqualTo(second) - } - - @Test - fun selectedEditorSnippetChangeStillChangesSuggestionKey() { - val first = computePromptSuggestionRequestKey( - request( - label = "first", - contextItems = editorContextItems( - symbolName = "main", - selection = true, - snippetTitle = "Selection", - snippetBody = "val answer = 42", - startLine = 10, - endLine = 16, - ), - ) - ) - val second = computePromptSuggestionRequestKey( - request( - label = "first", - contextItems = editorContextItems( - symbolName = "main", - selection = true, - snippetTitle = "Selection", - snippetBody = "return helper()", - startLine = 10, - endLine = 16, - ), - ) - ) - - assertThat(first).isNotEqualTo(second) - } - - @Test - fun nonEditorContextStillUsesExactSuggestionKey() { - val first = computePromptSuggestionRequestKey( - request( - label = "first", - contextItems = listOf( - contextItem( - rendererId = AgentPromptContextRendererIds.PATHS, - title = "paths", - body = "/work/project/src/App.kt", - source = "manual", - ) - ), - ) - ) - val second = computePromptSuggestionRequestKey( - request( - label = "first", - contextItems = listOf( - contextItem( - rendererId = AgentPromptContextRendererIds.PATHS, - title = "paths", - body = "/work/project/src/Other.kt", - source = "manual", - ) - ), - ) - ) - - assertThat(first).isNotEqualTo(second) - } - - private fun request( - label: String, - projectPath: String? = null, - targetModeId: String = "NEW_TASK", - contextItems: List = listOf(contextItem(rendererId = AgentPromptContextRendererIds.FILE, title = label)), - ): AgentPromptSuggestionRequest { - return AgentPromptSuggestionRequest( - project = ProjectManager.getInstance().defaultProject, - projectPath = projectPath, - targetModeId = targetModeId, - contextItems = contextItems, - ) - } - - private fun contextItem( - rendererId: String, - title: String? = rendererId, - body: String = "body", - source: String = "unknown", - ): AgentPromptContextItem { - return AgentPromptContextItem( - rendererId = rendererId, - title = title, - body = body, - source = source, - ) - } - - private fun editorContextItems( - symbolName: String?, - selection: Boolean, - snippetTitle: String, - snippetBody: String, - startLine: Int, - endLine: Int, - truncation: AgentPromptContextTruncation = AgentPromptContextTruncation.none(snippetBody.length), - ): List { - val filePath = "/work/project/src/App.kt" - return buildList { - add( - AgentPromptContextItem( - rendererId = AgentPromptContextRendererIds.FILE, - title = "File", - body = filePath, - payload = AgentPromptPayload.obj("path" to AgentPromptPayload.str(filePath)), - itemId = "editor.file", - source = "editor", - truncation = AgentPromptContextTruncation.none(filePath.length), - ) - ) - if (symbolName != null) { - add( - AgentPromptContextItem( - rendererId = AgentPromptContextRendererIds.SYMBOL, - title = "Symbol", - body = symbolName, - payload = AgentPromptPayload.obj("symbol" to AgentPromptPayload.str(symbolName)), - itemId = "editor.symbol", - parentItemId = "editor.file", - source = "editor", - truncation = AgentPromptContextTruncation.none(symbolName.length), - ) - ) - } - add( - AgentPromptContextItem( - rendererId = AgentPromptContextRendererIds.SNIPPET, - title = snippetTitle, - body = snippetBody, - payload = AgentPromptPayload.obj( - "startLine" to AgentPromptPayload.num(startLine), - "endLine" to AgentPromptPayload.num(endLine), - "selection" to AgentPromptPayload.bool(selection), - "language" to AgentPromptPayload.str("kotlin"), - ), - itemId = "editor.snippet", - parentItemId = "editor.file", - source = "editor", - truncation = truncation, - ) - ) - } - } - - private fun candidate( - id: String, - promptText: String = id, - provenance: AgentPromptSuggestionProvenance = AgentPromptSuggestionProvenance.TEMPLATE, - ): AgentPromptSuggestionCandidate { - return AgentPromptSuggestionCandidate( - id = id, - label = id, - promptText = promptText, - provenance = provenance, - ) - } - - @Suppress("RAW_SCOPE_CREATION") - private fun serviceScope(): CoroutineScope { - return CoroutineScope(SupervisorJob() + Dispatchers.Default) - } - - private fun waitForCondition(timeoutMs: Long = 5_000, condition: () -> Boolean) { - val deadline = System.currentTimeMillis() + timeoutMs - while (System.currentTimeMillis() < deadline) { - if (condition()) { - return - } - Thread.sleep(10) - } - error("Condition was not satisfied within ${timeoutMs}ms") - } -}