IJPL-248623 rename Agent Workbench chat UI to thread view

Replace the Agent Workbench product/API terminology that exposed chat as a top-level surface with Agent Thread View naming. This keeps the UI aligned with the persisted agent thread model and avoids mixing terminal/editor surfaces with an interaction paradigm.

The change intentionally drops legacy chat compatibility names, updates extension points, module names, API dumps, specs, and generated project metadata, while retaining tab wording only where it refers to IntelliJ editor-tab mechanics.

GitOrigin-RevId: 14fce40e99d40f6303e969d0b9394a801816bcf8
This commit is contained in:
Vladimir Krivosheev
2026-07-09 18:59:28 +00:00
committed by intellij-monorepo-bot
parent 4de1c0d870
commit 90d5ef0959
18 changed files with 2374 additions and 1309 deletions
+4 -4
View File
@@ -49,16 +49,14 @@
<module fileurl="file://$PROJECT_DIR$/fleet/util/serialization/fleet.util.serialization.iml" filepath="$PROJECT_DIR$/fleet/util/serialization/fleet.util.serialization.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/ai-review/intellij.agent.workbench.ai.review.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/ai-review/intellij.agent.workbench.ai.review.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/ai-review/intellij.agent.workbench.ai.review.tests.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/ai-review/intellij.agent.workbench.ai.review.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/chat/intellij.agent.workbench.chat.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/chat/intellij.agent.workbench.chat.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/chat/intellij.agent.workbench.chat.tests.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/chat/intellij.agent.workbench.chat.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/claude/awb/intellij.agent.workbench.claude.awb.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/claude/awb/intellij.agent.workbench.claude.awb.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/claude/awb/intellij.agent.workbench.claude.awb.tests.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/claude/awb/intellij.agent.workbench.claude.awb.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/codex/chat/intellij.agent.workbench.codex.chat.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/codex/chat/intellij.agent.workbench.codex.chat.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/codex/chat/intellij.agent.workbench.codex.chat.tests.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/codex/chat/intellij.agent.workbench.codex.chat.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/codex/ide/intellij.agent.workbench.codex.ide.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/codex/ide/intellij.agent.workbench.codex.ide.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/codex/ide/intellij.agent.workbench.codex.ide.tests.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/codex/ide/intellij.agent.workbench.codex.ide.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/codex/prompt-suggestions/intellij.agent.workbench.codex.prompt.suggestions.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/codex/prompt-suggestions/intellij.agent.workbench.codex.prompt.suggestions.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/codex/prompt-suggestions/intellij.agent.workbench.codex.prompt.suggestions.tests.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/codex/prompt-suggestions/intellij.agent.workbench.codex.prompt.suggestions.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/codex/thread-view/intellij.agent.workbench.codex.thread.view.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/codex/thread-view/intellij.agent.workbench.codex.thread.view.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/codex/thread-view/intellij.agent.workbench.codex.thread.view.tests.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/codex/thread-view/intellij.agent.workbench.codex.thread.view.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/engine/intellij.agent.workbench.engine.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/engine/intellij.agent.workbench.engine.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/engine/intellij.agent.workbench.engine.tests.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/engine/intellij.agent.workbench.engine.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/pi/awb/intellij.agent.workbench.pi.awb.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/pi/awb/intellij.agent.workbench.pi.awb.iml" />
@@ -90,6 +88,8 @@
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/sessions-toolwindow/intellij.agent.workbench.sessions.toolwindow.tests.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/sessions-toolwindow/intellij.agent.workbench.sessions.toolwindow.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/settings/intellij.agent.workbench.settings.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/settings/intellij.agent.workbench.settings.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/settings/intellij.agent.workbench.settings.tests.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/settings/intellij.agent.workbench.settings.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/thread-view/intellij.agent.workbench.thread.view.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/thread-view/intellij.agent.workbench.thread.view.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/thread-view/intellij.agent.workbench.thread.view.tests.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/thread-view/intellij.agent.workbench.thread.view.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/ui/intellij.agent.workbench.ui.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/ui/intellij.agent.workbench.ui.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/ui/intellij.agent.workbench.ui.tests.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/ui/intellij.agent.workbench.ui.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/agent-workbench/vcs-merge/intellij.agent.workbench.vcs.merge.iml" filepath="$PROJECT_DIR$/plugins/agent-workbench/vcs-merge/intellij.agent.workbench.vcs.merge.iml" />
@@ -9,8 +9,8 @@
<compilerArguments>
<stringArguments>
<stringArg name="jvmTarget" arg="25" />
<stringArg name="apiVersion" arg="2.3" />
<stringArg name="languageVersion" arg="2.3" />
<stringArg name="apiVersion" arg="2.4" />
<stringArg name="languageVersion" arg="2.4" />
</stringArguments>
<arrayArguments>
<arrayArg name="pluginClasspaths">
@@ -25,35 +25,33 @@
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="com.intellij.agent.workbench.acp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="module" module-name="intellij.platform.frontend" />
<orderEntry type="module" module-name="intellij.aiCodeReview" />
<orderEntry type="module" module-name="intellij.aiCodeReview.vcs" />
<orderEntry type="module" module-name="intellij.aiCodeReview.ui.shared" />
<orderEntry type="module" module-name="intellij.aiCodeReview.ui.frontend" />
<orderEntry type="module" module-name="intellij.libraries.kotlinx.serialization.core" />
<orderEntry type="module" module-name="intellij.libraries.kotlinx.serialization.json" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.platform.analysis" />
<orderEntry type="module" module-name="intellij.platform.ide" />
<orderEntry type="module" module-name="intellij.platform.ide.core" />
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
<orderEntry type="module" module-name="intellij.platform.projectModel" />
<orderEntry type="module" module-name="intellij.platform.core.ui" />
<orderEntry type="module" module-name="intellij.platform.editor.ui" />
<orderEntry type="module" module-name="intellij.platform.lang" />
<orderEntry type="module" module-name="intellij.platform.lang.impl" />
<orderEntry type="module" module-name="intellij.platform.ide" />
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
<orderEntry type="module" module-name="intellij.platform.ide.core" />
<orderEntry type="module" module-name="intellij.platform.execution.impl" />
<orderEntry type="module" module-name="intellij.platform.projectModel" />
<orderEntry type="module" module-name="intellij.platform.util" />
<orderEntry type="module" module-name="intellij.platform.diff" />
<orderEntry type="module" module-name="intellij.platform.diff.impl" />
<orderEntry type="module" module-name="intellij.platform.util.diff" />
<orderEntry type="module" module-name="intellij.libraries.kotlinx.coroutines.core" />
<orderEntry type="module" module-name="intellij.libraries.kotlinx.serialization.core" />
<orderEntry type="module" module-name="intellij.platform.vcs" />
<orderEntry type="module" module-name="intellij.platform.vcs.impl.shared" />
<orderEntry type="module" module-name="intellij.platform.vcs.shared" />
<orderEntry type="module" module-name="intellij.platform.vcs.log" />
<orderEntry type="module" module-name="intellij.platform.util.ui" />
<orderEntry type="module" module-name="intellij.platform.acp" />
<orderEntry type="module" module-name="intellij.platform.eel" />
<orderEntry type="module" module-name="intellij.terminal.frontend" />
<orderEntry type="module" module-name="intellij.terminal" />
<orderEntry type="module" module-name="intellij.agent.workbench.acp.runtime" />
<orderEntry type="module" module-name="intellij.agent.workbench.engine" />
<orderEntry type="module" module-name="intellij.platform.ai.agent.core" />
<orderEntry type="module" module-name="intellij.platform.ai.agent.sessions.core" />
<orderEntry type="module" module-name="intellij.agent.workbench.sessions.toolwindow" />
<orderEntry type="module" module-name="intellij.agent.workbench.thread.view" />
<orderEntry type="module" module-name="intellij.agent.workbench.ui" />
</component>
</module>
+2 -2
View File
@@ -42,7 +42,7 @@ jvm_library(
"//plugins/agent-workbench/lib-agent/core",
"//plugins/agent-workbench/prompt/core",
"//plugins/agent-workbench/lib-agent/sessions-core:ai-agent-sessions-core",
"//plugins/agent-workbench/chat",
"//plugins/agent-workbench/thread-view:agent-workbench-thread-view",
],
)
@@ -72,7 +72,7 @@ jvm_library(
"//plugins/agent-workbench/lib-agent/core:core_test_lib",
"//plugins/agent-workbench/prompt/core:core_test_lib",
"//plugins/agent-workbench/lib-agent/sessions-core:ai-agent-sessions-core_test_lib",
"//plugins/agent-workbench/chat:chat_test_lib",
"//plugins/agent-workbench/thread-view:agent-workbench-thread-view_test_lib",
],
)
### auto-generated section `build intellij.agent.workbench.engine` end
@@ -48,6 +48,6 @@
<orderEntry type="module" module-name="intellij.platform.ai.agent.core" />
<orderEntry type="module" module-name="intellij.agent.workbench.prompt.core" />
<orderEntry type="module" module-name="intellij.platform.ai.agent.sessions.core" />
<orderEntry type="module" module-name="intellij.agent.workbench.chat" />
<orderEntry type="module" module-name="intellij.agent.workbench.thread.view" />
</component>
</module>
@@ -5,7 +5,7 @@
<module name="intellij.platform.ai.agent.core"/>
<module name="intellij.agent.workbench.prompt.core"/>
<module name="intellij.platform.ai.agent.sessions.core"/>
<module name="intellij.agent.workbench.chat"/>
<module name="intellij.agent.workbench.thread.view"/>
</dependencies>
<extensionPoints>
@@ -22,21 +22,18 @@
- name: lib/modules/intellij.agent.workbench.ai.review.space.jar
contentModules:
- name: intellij.agent.workbench.ai.review.space
- name: lib/modules/intellij.agent.workbench.chat.jar
contentModules:
- name: intellij.agent.workbench.chat
- name: lib/modules/intellij.agent.workbench.claude.awb.jar
contentModules:
- name: intellij.agent.workbench.claude.awb
- name: lib/modules/intellij.agent.workbench.codex.chat.jar
contentModules:
- name: intellij.agent.workbench.codex.chat
- name: lib/modules/intellij.agent.workbench.codex.ide.jar
contentModules:
- name: intellij.agent.workbench.codex.ide
- name: lib/modules/intellij.agent.workbench.codex.prompt.suggestions.jar
contentModules:
- name: intellij.agent.workbench.codex.prompt.suggestions
- name: lib/modules/intellij.agent.workbench.codex.thread.view.jar
contentModules:
- name: intellij.agent.workbench.codex.thread.view
- name: lib/modules/intellij.agent.workbench.container.eel.jar
contentModules:
- name: intellij.agent.workbench.container.eel
@@ -98,6 +95,9 @@
- name: lib/modules/intellij.agent.workbench.settings.jar
contentModules:
- name: intellij.agent.workbench.settings
- name: lib/modules/intellij.agent.workbench.thread.view.jar
contentModules:
- name: intellij.agent.workbench.thread.view
- name: lib/modules/intellij.agent.workbench.ui.jar
contentModules:
- name: intellij.agent.workbench.ui
@@ -23,8 +23,8 @@ import com.intellij.openapi.application.UI
import com.intellij.openapi.components.service
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.util.Disposer
import com.intellij.platform.ai.agent.common.session.isClaudeMenuCommandPrompt
import com.intellij.platform.ai.agent.core.AgentThreadActivity
import com.intellij.platform.ai.agent.core.AgentThreadActivityReport
import com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode
import com.intellij.platform.ai.agent.core.session.AgentSessionProvider
import com.intellij.platform.ai.agent.core.session.AgentSessionThread
@@ -33,6 +33,7 @@ import com.intellij.platform.ai.agent.sessions.core.providers.AGENT_PROMPT_PROVI
import com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessagePlan
import com.intellij.platform.ai.agent.sessions.core.providers.AgentInitialMessageStartupPolicy
import com.intellij.platform.ai.agent.sessions.core.providers.AgentPromptProviderOption
import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionMenuCommand
import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProviderDescriptor
import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionSource
import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec
@@ -682,7 +683,7 @@ class AgentPromptPaletteSubmitControllerTest {
updatedAt = 100,
archived = false,
provider = AgentSessionProvider.from("codex"),
activity = AgentThreadActivity.PROCESSING,
activityReport = AgentThreadActivityReport(AgentThreadActivity.PROCESSING),
subAgents = emptyList(),
)
),
@@ -790,6 +791,12 @@ class AgentPromptPaletteSubmitControllerTest {
override val displayNameKey: String = if (provider == AgentSessionProvider.from("claude")) "provider.claude" else "provider.codex"
override val newSessionLabelKey: String = displayNameKey
override val promptOptions: List<AgentPromptProviderOption> = promptOptions
override val menuCommands: List<AgentSessionMenuCommand> = if (provider == AgentSessionProvider.from("claude")) {
listOf(AgentSessionMenuCommand("/mcp"), AgentSessionMenuCommand("/rename", "[title]"))
}
else {
emptyList()
}
override val sessionSource: AgentSessionSource
get() = error("Not required for this test")
override val cliMissingMessageKey: String = displayNameKey
@@ -808,10 +815,6 @@ class AgentPromptPaletteSubmitControllerTest {
override fun buildInitialMessagePlan(request: AgentPromptInitialMessageRequest): AgentInitialMessagePlan {
return initialMessagePlanBuilder(request)
}
override fun shouldStripContextForPrompt(prompt: String): Boolean {
return this.provider == AgentSessionProvider.from("claude") && prompt.isClaudeMenuCommandPrompt()
}
}
}
@@ -4,8 +4,11 @@ package com.intellij.agent.workbench.sessions.actions
// @spec community/plugins/agent-workbench/spec/sessions/agent-terminal-sessions.spec.md
// @spec community/plugins/agent-workbench/spec/actions/new-thread.spec.md
import com.intellij.agent.workbench.chat.AgentChatDeferredStartContent
import com.intellij.agent.workbench.chat.installAgentChatDeferredStartContent
import com.intellij.agent.workbench.thread.view.AgentThreadViewDeferredStartContent
import com.intellij.agent.workbench.prompt.core.AgentPromptContextEnvelopeFormatter
import com.intellij.agent.workbench.prompt.core.AgentPromptContextItem
import com.intellij.agent.workbench.prompt.core.AgentPromptContextResolverService
import com.intellij.agent.workbench.prompt.core.AgentPromptInitialMessageRequest
import com.intellij.agent.workbench.prompt.core.AgentPromptInvocationData
import com.intellij.agent.workbench.prompt.core.AgentPromptLaunchProfile
import com.intellij.agent.workbench.prompt.core.AgentPromptLaunchRequest
@@ -14,9 +17,11 @@ import com.intellij.agent.workbench.prompt.core.AgentPromptLauncherBridge
import com.intellij.agent.workbench.prompt.core.AgentPromptLaunchers
import com.intellij.agent.workbench.prompt.core.AgentPromptProjectPathCandidate
import com.intellij.agent.workbench.prompt.core.AgentPromptReusableSourceEntry
import com.intellij.agent.workbench.prompt.ui.emptyState.createAgentWorkbenchInlinePromptEditorHost
import com.intellij.agent.workbench.prompt.ui.emptyState.createAgentWorkbenchInlineNewThreadPromptComponent
import com.intellij.agent.workbench.sessions.AgentSessionsBundle
import com.intellij.agent.workbench.sessions.projectLabelForPath
import com.intellij.agent.workbench.sessions.newThreadActionText
import com.intellij.platform.ai.agent.sessions.core.providers.initialMessageRequestForLaunchProfile
import com.intellij.agent.workbench.sessions.statistics.AgentWorkbenchEntryPoint
import com.intellij.agent.workbench.sessions.service.AgentDeferredNewSessionHandle
@@ -26,21 +31,21 @@ 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.AgentSessionProviderDescriptor
import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProviders
import com.intellij.openapi.application.EDT
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.components.serviceAsync
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.application.EDT
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.util.registry.RegistryManager
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.Nls
internal const val AGENT_WORKBENCH_NEW_THREAD_INLINE_PROMPT_REGISTRY_KEY: String = "agent.workbench.new.thread.inline.prompt"
@@ -49,11 +54,33 @@ fun createNewThreadViaService(
profile: AgentPromptLaunchProfile,
currentProject: Project,
entryPoint: AgentWorkbenchEntryPoint,
) {
createNewThreadViaService(
path = path,
profile = profile,
currentProject = currentProject,
entryPoint = entryPoint,
invocationData = null,
)
}
internal fun createNewThreadViaService(
path: String,
profile: AgentPromptLaunchProfile,
currentProject: Project,
entryPoint: AgentWorkbenchEntryPoint,
invocationData: AgentPromptInvocationData?,
) {
val provider = AgentSessionProvider.from(profile.providerId)
val descriptor = AgentSessionProviders.find(provider)
if (!shouldOpenInlineNewThreadPrompt(descriptor)) {
createNewThreadDirectly(path = path, profile = profile, currentProject = currentProject, entryPoint = entryPoint)
if (!shouldOpenInlineNewThreadPrompt(descriptor = descriptor)) {
createNewThreadDirectly(
path = path,
profile = profile,
currentProject = currentProject,
entryPoint = entryPoint,
invocationData = invocationData,
)
return
}
service<AgentSessionsInlineNewThreadPromptService>().openInlinePrompt(
@@ -61,11 +88,13 @@ fun createNewThreadViaService(
profile = profile,
currentProject = currentProject,
entryPoint = entryPoint,
descriptor = checkNotNull(descriptor),
invocationData = invocationData,
)
}
private fun shouldOpenInlineNewThreadPrompt(descriptor: AgentSessionProviderDescriptor?): Boolean {
private fun shouldOpenInlineNewThreadPrompt(
descriptor: AgentSessionProviderDescriptor?,
): Boolean {
return shouldOpenInlineNewThreadPrompt(
registryEnabled = RegistryManager.getInstance().get(AGENT_WORKBENCH_NEW_THREAD_INLINE_PROMPT_REGISTRY_KEY).asBoolean(),
descriptor = descriptor,
@@ -82,16 +111,60 @@ private fun createNewThreadDirectly(
profile: AgentPromptLaunchProfile,
currentProject: Project,
entryPoint: AgentWorkbenchEntryPoint,
invocationData: AgentPromptInvocationData?,
) {
val normalizedPath = normalizeAgentWorkbenchPath(path)
service<AgentSessionLaunchService>().createNewSession(
path = path,
path = normalizedPath,
launchProfileId = profile.id,
entryPoint = entryPoint,
currentProject = currentProject,
initialMessageRequest = initialMessageRequestForLaunchProfile(profile),
initialMessageRequest = buildNewThreadInitialMessageRequest(
profile = profile,
projectPath = normalizedPath,
invocationData = invocationData,
),
)
}
internal fun buildNewThreadInitialMessageRequest(
profile: AgentPromptLaunchProfile,
projectPath: String,
invocationData: AgentPromptInvocationData?,
collectDefaultContext: (AgentPromptInvocationData) -> List<AgentPromptContextItem> = ::collectDefaultNewThreadContext,
): AgentPromptInitialMessageRequest {
val baseRequest = initialMessageRequestForLaunchProfile(profile)
val contextItems = invocationData?.let(collectDefaultContext).orEmpty()
if (contextItems.isEmpty()) {
return baseRequest
}
val normalizedProjectPath = normalizeAgentWorkbenchPath(projectPath)
val selection = AgentPromptContextEnvelopeFormatter.prepareContextEnvelopeSelection(
items = contextItems,
softCapChars = AgentPromptContextEnvelopeFormatter.DEFAULT_SOFT_CAP_CHARS,
projectPath = normalizedProjectPath,
)
val preparedSelection = if (selection.exceedsSoftCap) {
AgentPromptContextEnvelopeFormatter.autoTrimContextEnvelopeSelection(
selection = selection,
projectPath = normalizedProjectPath,
)
}
else {
selection
}
return baseRequest.copy(
projectPath = normalizedProjectPath,
contextItems = preparedSelection.items,
contextEnvelopeSummary = preparedSelection.summary,
)
}
private fun collectDefaultNewThreadContext(invocationData: AgentPromptInvocationData): List<AgentPromptContextItem> {
return invocationData.project.service<AgentPromptContextResolverService>().collectDefaultContext(invocationData)
}
@Service(Service.Level.APP)
internal class AgentSessionsInlineNewThreadPromptService internal constructor(
private val coroutineScope: CoroutineScope,
@@ -101,15 +174,15 @@ internal class AgentSessionsInlineNewThreadPromptService internal constructor(
profile: AgentPromptLaunchProfile,
currentProject: Project,
entryPoint: AgentWorkbenchEntryPoint,
descriptor: AgentSessionProviderDescriptor,
invocationData: AgentPromptInvocationData?,
) {
coroutineScope.launch(CoroutineName("Agent Workbench inline New Thread prompt")) {
coroutineScope.launch(Dispatchers.EDT + CoroutineName("Agent Workbench inline New Thread prompt")) {
openInlinePromptSuspending(
path = path,
profile = profile,
currentProject = currentProject,
entryPoint = entryPoint,
descriptor = descriptor,
invocationData = invocationData,
)
}
}
@@ -119,101 +192,108 @@ internal class AgentSessionsInlineNewThreadPromptService internal constructor(
profile: AgentPromptLaunchProfile,
currentProject: Project,
entryPoint: AgentWorkbenchEntryPoint,
descriptor: AgentSessionProviderDescriptor,
invocationData: AgentPromptInvocationData?,
) {
val normalizedPath = normalizeAgentWorkbenchPath(path)
val provider = AgentSessionProvider.from(profile.providerId)
var promptProject: Project? = null
val handleDeferred = CompletableDeferred<AgentDeferredNewSessionHandle>()
val handle = try {
serviceAsync<AgentSessionLaunchService>().createDeferredNewSession(
path = normalizedPath,
provider = provider,
mode = profile.launchMode,
entryPoint = entryPoint,
openedChatHandler = { project, _ -> promptProject = project },
launchProfileId = profile.id,
launchTargetId = profile.launchTargetId,
generationSettings = profile.generationSettings,
waitingTitle = AgentSessionsBundle.message("toolwindow.thread.preparing.title", providerDisplayName(descriptor)),
waitingMessage = AgentSessionsBundle.message("toolwindow.thread.preparing.body"),
waitingTitle = AgentSessionsBundle.message("toolwindow.thread.preparing.title"),
deferredStartContentProvider = { project ->
createInlinePromptContent(
project = project,
path = normalizedPath,
profile = profile,
entryPoint = entryPoint,
handleDeferred = handleDeferred,
invocationData = invocationData,
)
},
).handle
}
catch (e: CancellationException) {
handleDeferred.cancel()
throw e
}
catch (e: Throwable) {
handleDeferred.cancel()
LOG.warn("Failed to open inline New Thread prompt for ${profile.providerId}:$normalizedPath", e)
createNewThreadDirectly(path = normalizedPath, profile = profile, currentProject = currentProject, entryPoint = entryPoint)
createNewThreadDirectly(
path = normalizedPath,
profile = profile,
currentProject = currentProject,
entryPoint = entryPoint,
invocationData = invocationData,
)
return
}
if (handle == null) {
createNewThreadDirectly(path = normalizedPath, profile = profile, currentProject = currentProject, entryPoint = entryPoint)
return
}
try {
installInlinePromptContent(
project = promptProject ?: currentProject,
handleDeferred.cancel()
createNewThreadDirectly(
path = normalizedPath,
profile = profile,
currentProject = currentProject,
entryPoint = entryPoint,
handle = handle,
invocationData = invocationData,
)
return
}
catch (e: CancellationException) {
throw e
}
catch (e: Throwable) {
LOG.warn("Failed to install inline New Thread prompt for ${profile.providerId}:$normalizedPath", e)
handle.start(initialMessageRequestForLaunchProfile(profile))
}
handleDeferred.complete(handle)
}
private suspend fun installInlinePromptContent(
private fun createInlinePromptContent(
project: Project,
path: String,
profile: AgentPromptLaunchProfile,
entryPoint: AgentWorkbenchEntryPoint,
handle: AgentDeferredNewSessionHandle,
) {
withContext(Dispatchers.EDT) {
val launcher = InlineNewThreadPromptLauncherBridge(projectPath = path, handle = handle)
handleDeferred: CompletableDeferred<AgentDeferredNewSessionHandle>,
invocationData: AgentPromptInvocationData?,
): AgentThreadViewDeferredStartContent {
val launcher = InlineNewThreadPromptLauncherBridge(projectPath = path, handleProvider = { handleDeferred.await() })
val component = createAgentWorkbenchInlineNewThreadPromptComponent(
project = project,
invocationData = AgentPromptInvocationData(
project = project,
actionId = AgentWorkbenchActionIds.Sessions.MainToolbar.NEW_THREAD,
actionText = AgentSessionsBundle.message("action.AgentWorkbenchSessions.MainToolbar.NewThread.text"),
actionPlace = entryPoint.name,
invokedAtMs = System.currentTimeMillis(),
),
invocationData = (invocationData ?: defaultNewThreadInvocationData(project, entryPoint, profile.name)).copy(project = project),
launcherProvider = { launcher },
initialLaunchProfileId = profile.id,
)
val installed = installAgentChatDeferredStartContent(
project = project,
file = handle.file,
content = AgentChatDeferredStartContent(
component = component,
preferredFocusedComponent = component.preferredFocusedComponent,
disposeContent = { Disposer.dispose(component) },
),
)
if (!installed) {
Disposer.dispose(component)
throw IllegalStateException("Inline New Thread prompt could not be installed into ${handle.file}")
}
}
return AgentThreadViewDeferredStartContent(
component = createAgentWorkbenchInlinePromptEditorHost(component),
preferredFocusedComponent = component.preferredFocusedComponent,
disposeContent = { Disposer.dispose(component) },
)
}
}
private fun defaultNewThreadInvocationData(
project: Project,
entryPoint: AgentWorkbenchEntryPoint,
profileName: @Nls String,
): AgentPromptInvocationData {
return AgentPromptInvocationData(
project = project,
actionId = AgentWorkbenchActionIds.Sessions.MainToolbar.NEW_THREAD,
actionText = newThreadActionText(profileName),
actionPlace = entryPoint.name,
invokedAtMs = System.currentTimeMillis(),
)
}
private class InlineNewThreadPromptLauncherBridge(
private val projectPath: String,
private val handle: AgentDeferredNewSessionHandle,
private val handleProvider: suspend () -> AgentDeferredNewSessionHandle,
private val delegateProvider: () -> AgentPromptLauncherBridge? = AgentPromptLaunchers::find,
) : AgentPromptLauncherBridge {
override suspend fun launch(request: AgentPromptLaunchRequest): AgentPromptLaunchResult {
return handle.launch(request)
return handleProvider().launch(request)
}
override fun loadProviderPreferences(): AgentPromptLauncherBridge.ProviderPreferences {
@@ -249,9 +329,4 @@ private class InlineNewThreadPromptLauncherBridge(
}
}
private fun providerDisplayName(descriptor: AgentSessionProviderDescriptor): @NlsSafe String {
return runCatching { AgentSessionsBundle.message(descriptor.displayNameKey) }
.getOrDefault(descriptor.displayNameFallback)
}
private val LOG = logger<AgentSessionsInlineNewThreadPromptService>()
@@ -1,7 +1,7 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.agent.workbench.sessions
import com.intellij.agent.workbench.chat.AgentChatEditorTabActionContext
import com.intellij.agent.workbench.thread.view.AgentThreadViewEditorTabActionContext
import com.intellij.agent.workbench.ui.AgentWorkbenchActionIds
import com.intellij.platform.ai.agent.core.normalizeAgentWorkbenchPath
import com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode
@@ -1122,22 +1122,22 @@ class AgentSessionsMainToolbarNewThreadActionsTest {
}
@Test
fun projectMainToolbarContextPrefersSelectedChatSourcePathOverProjectBasePath() {
fun projectMainToolbarContextPrefersSelectedThreadViewSourcePathOverProjectBasePath() {
val project = sourceProjectProxy()
val event = eventWithProject(project)
val context = resolveAgentSessionsMainToolbarNewThreadContext(
event = event,
isDedicatedProject = { false },
selectedSourcePath = { "/work/chat-repo" },
selectedSourcePath = { "/work/threadView-repo" },
)
val target = checkNotNull(context).target as AgentSessionsNewThreadTarget.Direct
assertThat(target.path).isEqualTo("/work/chat-repo")
assertThat(target.path).isEqualTo("/work/threadView-repo")
}
@Test
fun projectMainToolbarContextUsesProjectBasePathWhenNoSelectedChatSourcePathExists() {
fun projectMainToolbarContextUsesProjectBasePathWhenNoSelectedThreadViewSourcePathExists() {
val project = sourceProjectProxy()
val event = eventWithProject(project)
@@ -1152,19 +1152,19 @@ class AgentSessionsMainToolbarNewThreadActionsTest {
}
@Test
fun projectMainToolbarContextPrefersEventChatContextWhenAvailable() {
fun projectMainToolbarContextPrefersEventThreadViewContextWhenAvailable() {
val project = sourceProjectProxy()
val event = eventWithProject(project)
val context = resolveAgentSessionsMainToolbarNewThreadContext(
event = event,
isDedicatedProject = { false },
resolveChatContext = { editorContext() },
selectedSourcePath = { "/work/selected-chat-repo" },
resolveThreadViewContext = { editorContext() },
selectedSourcePath = { "/work/selected-threadView-repo" },
)
val target = checkNotNull(context).target as AgentSessionsNewThreadTarget.Direct
assertThat(target.path).isEqualTo("/work/event-chat-repo")
assertThat(target.path).isEqualTo("/work/event-threadView-repo")
}
@Test
@@ -1224,10 +1224,10 @@ class AgentSessionsMainToolbarNewThreadActionsTest {
private const val MANAGE_LAUNCH_PROFILES_TEXT: String = "Manage Launch Profiles…"
private fun editorContext(): AgentChatEditorTabActionContext {
val path = "/work/event-chat-repo"
private fun editorContext(): AgentThreadViewEditorTabActionContext {
val path = "/work/event-threadView-repo"
val normalizedPath = normalizeAgentWorkbenchPath(path)
return AgentChatEditorTabActionContext(
return AgentThreadViewEditorTabActionContext(
project = ProjectManager.getInstance().defaultProject,
path = normalizedPath,
tabKey = "codex:$normalizedPath:thread-1",
File diff suppressed because it is too large Load Diff
@@ -1,11 +1,11 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.agent.workbench.sessions
import com.intellij.agent.workbench.chat.AgentChatPendingTabRebindOutcome
import com.intellij.agent.workbench.chat.AgentChatPendingTabRebindReport
import com.intellij.agent.workbench.chat.AgentChatPendingTabRebindRequest
import com.intellij.agent.workbench.chat.AgentChatPendingTabRebindStatus
import com.intellij.agent.workbench.chat.AgentChatPendingTabSnapshot
import com.intellij.agent.workbench.thread.view.AgentThreadViewPendingTabRebindOutcome
import com.intellij.agent.workbench.thread.view.AgentThreadViewPendingTabRebindReport
import com.intellij.agent.workbench.thread.view.AgentThreadViewPendingTabRebindRequest
import com.intellij.agent.workbench.thread.view.AgentThreadViewPendingTabRebindStatus
import com.intellij.agent.workbench.thread.view.AgentThreadViewPendingTabSnapshot
import com.intellij.platform.ai.agent.core.session.AgentSessionProvider
import com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode
import com.intellij.platform.ai.agent.core.session.AgentSessionThread
@@ -17,6 +17,9 @@ import com.intellij.agent.workbench.prompt.core.AgentPromptLaunchError
import com.intellij.agent.workbench.prompt.core.AgentPromptLaunchRequest
import com.intellij.agent.workbench.prompt.core.AgentPromptLaunchResult
import com.intellij.agent.workbench.prompt.core.AgentPromptReasoningEffort
import com.intellij.platform.ai.agent.sessions.core.launch.AGENT_SESSION_SURFACE_ACP
import com.intellij.platform.ai.agent.sessions.core.launch.AGENT_SESSION_SURFACE_TERMINAL
import com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionSurfaces
import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProviders
import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionTerminalLaunchSpec
import com.intellij.platform.ai.agent.sessions.core.providers.InMemoryAgentSessionProviderRegistry
@@ -27,7 +30,9 @@ import com.intellij.agent.workbench.sessions.state.AgentSessionLaunchProfileStat
import com.intellij.agent.workbench.sessions.state.AgentSessionUiPreferencesStateService
import com.intellij.agent.workbench.sessions.util.buildAgentSessionIdentity
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.testFramework.junit5.TestApplication
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
@@ -59,7 +64,7 @@ class AgentSessionLaunchServiceTest {
true
},
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
val sessionSource = sourceForActiveThreads(activeThreads)
AgentSessionProviders.withRegistryForTest(InMemoryAgentSessionProviderRegistry(listOf(descriptor))) {
@@ -67,22 +72,22 @@ class AgentSessionLaunchServiceTest {
withTestServiceAndLaunch(
sessionSourcesProvider = { listOf(sessionSource) },
projectEntriesProvider = { listOf(openTestProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
archivedSessionsRefreshIfLoaded = { archivedRefreshCalls.incrementAndGet() },
) { service, launchService ->
launchService.openChatThread(
launchService.openThreadViewThread(
path = PROJECT_PATH,
thread = thread(id = "codex-archived", updatedAt = 200, provider = AgentSessionProvider.from("codex")).copy(archived = true),
entryPoint = AgentWorkbenchEntryPoint.TREE_ROW,
)
waitForCondition {
chatOpenExecutor.openChatCalls.get() == 1 &&
threadViewOpenExecutor.openThreadViewCalls.get() == 1 &&
archivedRefreshCalls.get() == 1 &&
activeThreadIds(service.state.value).contains("codex-archived")
}
val openRequest = checkNotNull(chatOpenExecutor.lastOpenChatRequest.get())
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenThreadViewRequest.get())
assertThat(openRequest.thread.id).isEqualTo("codex-archived")
assertThat(openRequest.thread.archived).isFalse()
assertThat(openRequest.subAgent).isNull()
@@ -103,25 +108,25 @@ class AgentSessionLaunchServiceTest {
true
},
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(InMemoryAgentSessionProviderRegistry(listOf(descriptor))) {
runBlocking(Dispatchers.Default) {
withTestServiceAndLaunch(
sessionSourcesProvider = { listOf(sourceForActiveThreads(emptyList())) },
projectEntriesProvider = { listOf(openTestProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
archivedSessionsRefreshIfLoaded = { archivedRefreshCalls.incrementAndGet() },
) { _, launchService ->
launchService.openChatThread(
launchService.openThreadViewThread(
path = PROJECT_PATH,
thread = thread(id = "codex-active", updatedAt = 200, provider = AgentSessionProvider.from("codex")),
entryPoint = AgentWorkbenchEntryPoint.TREE_ROW,
)
waitForCondition { chatOpenExecutor.openChatCalls.get() == 1 }
waitForCondition { threadViewOpenExecutor.openThreadViewCalls.get() == 1 }
val openRequest = checkNotNull(chatOpenExecutor.lastOpenChatRequest.get())
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenThreadViewRequest.get())
assertThat(openRequest.thread.id).isEqualTo("codex-active")
assertThat(openRequest.thread.archived).isFalse()
assertThat(unarchiveCalls.get()).isZero()
@@ -131,13 +136,44 @@ class AgentSessionLaunchServiceTest {
}
}
@Test
fun openThreadViewThreadPassesOpenedThreadViewHandlerToExecutor() {
val descriptor = testDescriptor(
supportsUnarchiveThread = false,
unarchiveThreadHandler = { _, _ -> false },
)
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
val openedThreadViewHandler: suspend (Project, VirtualFile) -> Unit = { _, _ -> }
AgentSessionProviders.withRegistryForTest(InMemoryAgentSessionProviderRegistry(listOf(descriptor))) {
runBlocking(Dispatchers.Default) {
withTestServiceAndLaunch(
sessionSourcesProvider = { listOf(sourceForActiveThreads(emptyList())) },
projectEntriesProvider = { listOf(openTestProjectEntry(PROJECT_PATH, "Project A")) },
threadViewOpenExecutor = threadViewOpenExecutor,
) { _, launchService ->
launchService.openThreadViewThread(
path = PROJECT_PATH,
thread = thread(id = "codex-active", updatedAt = 200, provider = AgentSessionProvider.from("codex")),
entryPoint = AgentWorkbenchEntryPoint.TREE_ROW,
openedThreadViewHandler = openedThreadViewHandler,
)
waitForCondition { threadViewOpenExecutor.openThreadViewCalls.get() == 1 }
assertThat(threadViewOpenExecutor.lastOpenThreadViewHandler.get()).isSameAs(openedThreadViewHandler)
}
}
}
}
@Test
fun openThreadWithBranchMismatchAllowsDialogToAcquireWriteIntentReadLock() {
val descriptor = testDescriptor(
supportsUnarchiveThread = false,
unarchiveThreadHandler = { _, _ -> false },
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
val confirmations = AtomicInteger(0)
val currentProject = ProjectManager.getInstance().defaultProject
@@ -146,7 +182,7 @@ class AgentSessionLaunchServiceTest {
withTestServiceAndLaunch(
sessionSourcesProvider = { listOf(sourceForActiveThreads(emptyList())) },
projectEntriesProvider = { listOf(featureWorktreeProjectEntry()) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
branchMismatchConfirmation = { project, originBranch, currentBranch ->
confirmations.incrementAndGet()
assertThat(project).isSameAs(currentProject)
@@ -162,14 +198,14 @@ class AgentSessionLaunchServiceTest {
service.refreshCatalogAndLoadNewlyOpened()
waitForCondition { hasFeatureWorktree(service.state.value) }
launchService.openChatThread(
launchService.openThreadViewThread(
path = WORKTREE_PATH,
thread = branchMismatchThread(),
entryPoint = AgentWorkbenchEntryPoint.TREE_ROW,
currentProject = currentProject,
)
waitForCondition { chatOpenExecutor.openChatCalls.get() == 1 }
waitForCondition { threadViewOpenExecutor.openThreadViewCalls.get() == 1 }
assertThat(confirmations.get()).isEqualTo(1)
}
}
@@ -182,7 +218,7 @@ class AgentSessionLaunchServiceTest {
supportsUnarchiveThread = false,
unarchiveThreadHandler = { _, _ -> false },
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
val confirmations = AtomicInteger(0)
val promptResults = CopyOnWriteArrayList<AgentPromptLaunchResult>()
@@ -191,7 +227,7 @@ class AgentSessionLaunchServiceTest {
withTestServiceAndLaunch(
sessionSourcesProvider = { listOf(sourceForActiveThreads(emptyList())) },
projectEntriesProvider = { listOf(featureWorktreeProjectEntry()) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
branchMismatchConfirmation = { _, originBranch, currentBranch ->
confirmations.incrementAndGet()
assertThat(originBranch).isEqualTo("main")
@@ -202,7 +238,7 @@ class AgentSessionLaunchServiceTest {
service.refreshCatalogAndLoadNewlyOpened()
waitForCondition { hasFeatureWorktree(service.state.value) }
launchService.openChatThread(
launchService.openThreadViewThread(
path = WORKTREE_PATH,
thread = branchMismatchThread(),
entryPoint = AgentWorkbenchEntryPoint.TREE_ROW,
@@ -210,7 +246,7 @@ class AgentSessionLaunchServiceTest {
)
waitForCondition { promptResults.singleOrNull()?.error == AgentPromptLaunchError.CANCELLED }
assertThat(chatOpenExecutor.openChatCalls.get()).isZero()
assertThat(threadViewOpenExecutor.openThreadViewCalls.get()).isZero()
assertThat(confirmations.get()).isEqualTo(1)
}
}
@@ -224,24 +260,24 @@ class AgentSessionLaunchServiceTest {
unarchiveThreadHandler = { _, _ -> false },
supportedModes = setOf(AgentSessionLaunchMode.STANDARD, AgentSessionLaunchMode.YOLO),
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(InMemoryAgentSessionProviderRegistry(listOf(descriptor))) {
runBlocking(Dispatchers.Default) {
withTestServiceAndLaunch(
sessionSourcesProvider = { listOf(sourceForActiveThreads(emptyList())) },
projectEntriesProvider = { listOf(openTestProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { _, launchService ->
launchService.openChatThread(
launchService.openThreadViewThread(
path = PROJECT_PATH,
thread = thread(id = "codex-active", updatedAt = 200, provider = AgentSessionProvider.from("codex")),
entryPoint = AgentWorkbenchEntryPoint.TREE_ROW,
)
waitForCondition { chatOpenExecutor.openChatCalls.get() == 1 }
waitForCondition { threadViewOpenExecutor.openThreadViewCalls.get() == 1 }
val openRequest = checkNotNull(chatOpenExecutor.lastOpenChatRequest.get())
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenThreadViewRequest.get())
assertThat(openRequest.launchMode).isNull()
}
}
@@ -255,7 +291,7 @@ class AgentSessionLaunchServiceTest {
unarchiveThreadHandler = { _, _ -> false },
supportedModes = setOf(AgentSessionLaunchMode.STANDARD, AgentSessionLaunchMode.YOLO),
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
val activeThread = thread(id = "codex-active", updatedAt = 200, provider = AgentSessionProvider.from("codex"))
AgentSessionProviders.withRegistryForTest(InMemoryAgentSessionProviderRegistry(listOf(descriptor))) {
@@ -263,7 +299,7 @@ class AgentSessionLaunchServiceTest {
withTestServiceAndLaunch(
sessionSourcesProvider = { listOf(sourceForActiveThreads(listOf(activeThread))) },
projectEntriesProvider = { listOf(openTestProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { service, launchService ->
service.refresh()
waitForCondition { activeThreadIds(service.state.value).contains(activeThread.id) }
@@ -280,9 +316,9 @@ class AgentSessionLaunchServiceTest {
assertThat(result.launched).isTrue()
assertThat(result.error).isNull()
waitForCondition { chatOpenExecutor.openChatCalls.get() == 1 }
waitForCondition { threadViewOpenExecutor.openThreadViewCalls.get() == 1 }
val openRequest = checkNotNull(chatOpenExecutor.lastOpenChatRequest.get())
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenThreadViewRequest.get())
assertThat(openRequest.thread.id).isEqualTo(activeThread.id)
assertThat(openRequest.launchMode).isEqualTo(AgentSessionLaunchMode.YOLO)
}
@@ -307,10 +343,12 @@ class AgentSessionLaunchServiceTest {
name = "Codex Yolo",
providerId = AgentSessionProvider.from("codex").value,
launchMode = AgentSessionLaunchMode.YOLO,
launchTargetId = "codex.test.target",
surfaceId = AGENT_SESSION_SURFACE_TERMINAL,
generationSettings = profileSettings,
)
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
val activeThread = thread(id = "codex-active", updatedAt = 200, provider = AgentSessionProvider.from("codex"))
AgentSessionProviders.withRegistryForTest(InMemoryAgentSessionProviderRegistry(listOf(descriptor))) {
@@ -319,7 +357,7 @@ class AgentSessionLaunchServiceTest {
sessionSourcesProvider = { listOf(sourceForActiveThreads(listOf(activeThread))) },
projectEntriesProvider = { listOf(openTestProjectEntry(PROJECT_PATH, "Project A")) },
uiPreferencesState = uiPreferencesState,
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { service, launchService ->
service.refresh()
waitForCondition { activeThreadIds(service.state.value).contains(activeThread.id) }
@@ -330,6 +368,7 @@ class AgentSessionLaunchServiceTest {
provider = AgentSessionProvider.from("codex"),
projectPath = PROJECT_PATH,
launchMode = AgentSessionLaunchMode.STANDARD,
surfaceId = AGENT_SESSION_SURFACE_ACP,
initialMessageRequest = AgentPromptInitialMessageRequest(prompt = "Continue this thread"),
targetThreadId = activeThread.id,
)
@@ -337,12 +376,14 @@ class AgentSessionLaunchServiceTest {
assertThat(result.launched).isTrue()
assertThat(result.error).isNull()
waitForCondition { chatOpenExecutor.openChatCalls.get() == 1 }
waitForCondition { threadViewOpenExecutor.openThreadViewCalls.get() == 1 }
val openRequest = checkNotNull(chatOpenExecutor.lastOpenChatRequest.get())
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenThreadViewRequest.get())
assertThat(openRequest.thread.id).isEqualTo(activeThread.id)
assertThat(openRequest.launchMode).isEqualTo(AgentSessionLaunchMode.YOLO)
assertThat(openRequest.launchProfileId).isEqualTo(profileId)
assertThat(openRequest.launchTargetId).isEqualTo("codex.test.target")
assertThat(openRequest.surfaceId).isEqualTo(AgentSessionSurfaces.TERMINAL)
assertThat(openRequest.generationSettings).isEqualTo(profileSettings)
}
}
@@ -366,10 +407,12 @@ class AgentSessionLaunchServiceTest {
name = "Codex New Yolo",
providerId = AgentSessionProvider.from("codex").value,
launchMode = AgentSessionLaunchMode.YOLO,
launchTargetId = "codex.active.target",
surfaceId = AGENT_SESSION_SURFACE_TERMINAL,
generationSettings = profileSettings,
)
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(InMemoryAgentSessionProviderRegistry(listOf(descriptor))) {
runBlocking(Dispatchers.Default) {
@@ -377,7 +420,7 @@ class AgentSessionLaunchServiceTest {
sessionSourcesProvider = { listOf(descriptor.sessionSource) },
projectEntriesProvider = { listOf(openTestProjectEntry(PROJECT_PATH, "Project A")) },
uiPreferencesState = uiPreferencesState,
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { _, launchService ->
launchService.createNewSession(
path = PROJECT_PATH,
@@ -385,11 +428,13 @@ class AgentSessionLaunchServiceTest {
entryPoint = AgentWorkbenchEntryPoint.TREE_ROW,
)
waitForCondition { chatOpenExecutor.openNewChatCalls.get() == 1 }
val openRequest = checkNotNull(chatOpenExecutor.lastOpenNewChatRequest.get())
waitForCondition { threadViewOpenExecutor.openNewThreadViewCalls.get() == 1 }
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenNewThreadViewRequest.get())
assertThat(openRequest.identity).startsWith("codex:new-")
assertThat(openRequest.launchMode).isEqualTo(AgentSessionLaunchMode.YOLO)
assertThat(openRequest.launchProfileId).isEqualTo(profileId)
assertThat(openRequest.launchTargetId).isEqualTo("codex.active.target")
assertThat(openRequest.surfaceId).isEqualTo(AgentSessionSurfaces.TERMINAL)
assertThat(openRequest.generationSettings).isEqualTo(profileSettings)
}
}
@@ -397,7 +442,7 @@ class AgentSessionLaunchServiceTest {
}
@Test
fun createNewSessionOpensPreparingChatBeforeLaunchSpecIsPrepared() {
fun createNewSessionOpensPreparingThreadViewBeforeLaunchSpecIsPrepared() {
val launchSpecRequested = CompletableDeferred<Unit>()
val releaseLaunchSpec = CompletableDeferred<Unit>()
val descriptor = TestAgentSessionProviderDescriptor(
@@ -410,14 +455,14 @@ class AgentSessionLaunchServiceTest {
AgentSessionTerminalLaunchSpec(command = listOf("test", "new", mode.name))
},
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(InMemoryAgentSessionProviderRegistry(listOf(descriptor))) {
runBlocking(Dispatchers.Default) {
withTestServiceAndLaunch(
sessionSourcesProvider = { listOf(descriptor.sessionSource) },
projectEntriesProvider = { listOf(openTestProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { _, launchService ->
launchService.createNewSession(
path = PROJECT_PATH,
@@ -426,27 +471,131 @@ class AgentSessionLaunchServiceTest {
entryPoint = AgentWorkbenchEntryPoint.TREE_ROW,
)
chatOpenExecutor.awaitOpenPreparingNewChatCalls(1)
threadViewOpenExecutor.awaitOpenPreparingNewThreadViewCalls(1)
val preparingRequest = checkNotNull(threadViewOpenExecutor.lastOpenPreparingNewThreadViewRequest.get())
assertThat(preparingRequest.hasDeferredStartContentProvider).isFalse()
assertThat(preparingRequest.surfaceId).isEqualTo(AgentSessionSurfaces.TERMINAL)
withTimeout(5_000.milliseconds) { launchSpecRequested.await() }
assertThat(chatOpenExecutor.openNewChatCalls.get()).isZero()
assertThat(threadViewOpenExecutor.openNewThreadViewCalls.get()).isZero()
releaseLaunchSpec.complete(Unit)
chatOpenExecutor.awaitOpenNewChatCalls(1)
val openRequest = checkNotNull(chatOpenExecutor.lastOpenNewChatRequest.get())
threadViewOpenExecutor.awaitOpenNewThreadViewCalls(1)
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenNewThreadViewRequest.get())
assertThat(openRequest.launchSpec.command).containsExactly("test", "new", AgentSessionLaunchMode.STANDARD.name)
assertThat(openRequest.surfaceId).isEqualTo(AgentSessionSurfaces.TERMINAL)
}
}
}
}
@Test
fun createNewSessionReportsPreparationFailureInOpenedChat() {
fun createNewSessionDefaultsToDescriptorSurface() {
val descriptor = TestAgentSessionProviderDescriptor(
provider = AgentSessionProvider.from("acp"),
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
cliAvailable = true,
defaultLaunchSurface = AgentSessionSurfaces.ACP,
supportedLaunchSurfaces = setOf(AgentSessionSurfaces.ACP),
)
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(InMemoryAgentSessionProviderRegistry(listOf(descriptor))) {
runBlocking(Dispatchers.Default) {
withTestServiceAndLaunch(
sessionSourcesProvider = { listOf(descriptor.sessionSource) },
projectEntriesProvider = { listOf(openTestProjectEntry(PROJECT_PATH, "Project A")) },
threadViewOpenExecutor = threadViewOpenExecutor,
) { _, launchService ->
launchService.createNewSession(
path = PROJECT_PATH,
provider = AgentSessionProvider.from("acp"),
mode = AgentSessionLaunchMode.STANDARD,
entryPoint = AgentWorkbenchEntryPoint.TREE_ROW,
)
threadViewOpenExecutor.awaitOpenNewThreadViewCalls(1)
val preparingRequest = checkNotNull(threadViewOpenExecutor.lastOpenPreparingNewThreadViewRequest.get())
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenNewThreadViewRequest.get())
assertThat(preparingRequest.surfaceId).isEqualTo(AgentSessionSurfaces.ACP)
assertThat(openRequest.surfaceId).isEqualTo(AgentSessionSurfaces.ACP)
}
}
}
}
@Test
fun createNewSessionUsesGenericDeferredWaitingCopy() {
val descriptor = TestAgentSessionProviderDescriptor(
provider = AgentSessionProvider.from("codex"),
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
cliAvailable = true,
)
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(InMemoryAgentSessionProviderRegistry(listOf(descriptor))) {
runBlocking(Dispatchers.Default) {
withTestServiceAndLaunch(
sessionSourcesProvider = { listOf(descriptor.sessionSource) },
projectEntriesProvider = { listOf(openTestProjectEntry(PROJECT_PATH, "Project A")) },
threadViewOpenExecutor = threadViewOpenExecutor,
) { _, launchService ->
launchService.createNewSession(
path = PROJECT_PATH,
provider = AgentSessionProvider.from("codex"),
mode = AgentSessionLaunchMode.STANDARD,
entryPoint = AgentWorkbenchEntryPoint.PROMPT,
)
threadViewOpenExecutor.awaitOpenPreparingNewThreadViewCalls(1)
val state = checkNotNull(threadViewOpenExecutor.lastOpenPreparingNewThreadViewRequest.get()).waitingState
assertThat(state.title).isEqualTo("Starting new thread…")
assertThat(state.message).isNull()
}
}
}
}
@Test
fun createDeferredNewSessionPassesDeferredStartContentProviderToPreparingThreadView() {
val descriptor = TestAgentSessionProviderDescriptor(
provider = AgentSessionProvider.from("codex"),
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
cliAvailable = true,
)
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(InMemoryAgentSessionProviderRegistry(listOf(descriptor))) {
runBlocking(Dispatchers.Default) {
withTestServiceAndLaunch(
sessionSourcesProvider = { listOf(descriptor.sessionSource) },
projectEntriesProvider = { listOf(openTestProjectEntry(PROJECT_PATH, "Project A")) },
threadViewOpenExecutor = threadViewOpenExecutor,
) { _, launchService ->
val result = launchService.createDeferredNewSession(
path = PROJECT_PATH,
provider = AgentSessionProvider.from("codex"),
mode = AgentSessionLaunchMode.STANDARD,
entryPoint = AgentWorkbenchEntryPoint.PROMPT,
waitingTitle = "Preparing",
deferredStartContentProvider = { error("test executor records the provider without rendering it") },
)
assertThat(result.handle).isNotNull()
threadViewOpenExecutor.awaitOpenPreparingNewThreadViewCalls(1)
assertThat(checkNotNull(threadViewOpenExecutor.lastOpenPreparingNewThreadViewRequest.get()).hasDeferredStartContentProvider).isTrue()
}
}
}
}
@Test
fun createNewSessionReportsPreparationFailureInOpenedThreadView() {
val descriptor = TestAgentSessionProviderDescriptor(
provider = AgentSessionProvider.from("codex"),
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
cliAvailable = false,
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
val launchResult = CompletableDeferred<AgentPromptLaunchResult>()
AgentSessionProviders.withRegistryForTest(InMemoryAgentSessionProviderRegistry(listOf(descriptor))) {
@@ -454,7 +603,7 @@ class AgentSessionLaunchServiceTest {
withTestServiceAndLaunch(
sessionSourcesProvider = { listOf(descriptor.sessionSource) },
projectEntriesProvider = { listOf(openTestProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { _, launchService ->
launchService.createNewSession(
path = PROJECT_PATH,
@@ -464,13 +613,13 @@ class AgentSessionLaunchServiceTest {
promptLaunchResolved = { result -> launchResult.complete(result) },
)
chatOpenExecutor.awaitOpenPreparingNewChatCalls(1)
waitForCondition { chatOpenExecutor.failPreparingNewChatCalls.get() == 1 }
threadViewOpenExecutor.awaitOpenPreparingNewThreadViewCalls(1)
waitForCondition { threadViewOpenExecutor.failPreparingNewThreadViewCalls.get() == 1 }
val result = withTimeout(5_000.milliseconds) { launchResult.await() }
assertThat(result.launched).isFalse()
assertThat(result.error).isEqualTo(AgentPromptLaunchError.PROVIDER_UNAVAILABLE)
assertThat(chatOpenExecutor.openNewChatCalls.get()).isZero()
assertThat(chatOpenExecutor.lastFailPreparingNewChatMessage.get()).isNotBlank()
assertThat(threadViewOpenExecutor.openNewThreadViewCalls.get()).isZero()
assertThat(threadViewOpenExecutor.lastFailPreparingNewThreadViewMessage.get()).isNotBlank()
}
}
}
@@ -483,7 +632,7 @@ class AgentSessionLaunchServiceTest {
provider = AgentSessionProvider.from("codex"),
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
cliAvailable = true,
newSessionLaunchSpecProvider = {
newSessionLaunchSpecProvider = { _ ->
val attempt = launchSpecAttempts.incrementAndGet()
AgentSessionTerminalLaunchSpec(command = listOf("test", "retry", attempt.toString()))
},
@@ -491,14 +640,14 @@ class AgentSessionLaunchServiceTest {
override val supportedReasoningEfforts: Set<AgentPromptReasoningEffort>
get() = setOf(AgentPromptReasoningEffort.HIGH)
}
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(InMemoryAgentSessionProviderRegistry(listOf(descriptor))) {
runBlocking(Dispatchers.Default) {
withTestServiceAndLaunch(
sessionSourcesProvider = { listOf(descriptor.sessionSource) },
projectEntriesProvider = { listOf(openTestProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { _, launchService ->
val handle = checkNotNull(
launchService.createDeferredNewSession(
@@ -509,7 +658,7 @@ class AgentSessionLaunchServiceTest {
waitingTitle = "Preparing",
).handle
)
chatOpenExecutor.awaitOpenPreparingNewChatCalls(1)
threadViewOpenExecutor.awaitOpenPreparingNewThreadViewCalls(1)
val rejectedRequest = AgentPromptLaunchRequest(
provider = AgentSessionProvider.from("codex"),
@@ -530,16 +679,16 @@ class AgentSessionLaunchServiceTest {
val failedResult = handle.launch(rejectedRequest)
assertThat(failedResult.launched).isFalse()
assertThat(failedResult.error).isEqualTo(AgentPromptLaunchError.UNSUPPORTED_LAUNCH_MODE)
assertThat(chatOpenExecutor.openNewChatCalls.get()).isZero()
assertThat(chatOpenExecutor.failPreparingNewChatCalls.get()).isZero()
assertThat(threadViewOpenExecutor.openNewThreadViewCalls.get()).isZero()
assertThat(threadViewOpenExecutor.failPreparingNewThreadViewCalls.get()).isZero()
assertThat(launchSpecAttempts.get()).isZero()
val successfulResult = handle.launch(request)
assertThat(successfulResult.launched).isTrue()
assertThat(successfulResult.error).isNull()
chatOpenExecutor.awaitOpenNewChatCalls(1)
threadViewOpenExecutor.awaitOpenNewThreadViewCalls(1)
val openRequest = checkNotNull(chatOpenExecutor.lastOpenNewChatRequest.get())
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenNewThreadViewRequest.get())
assertThat(openRequest.launchSpec.command).containsExactly("test", "retry", "1")
assertThat(openRequest.initialComposedMessage).isEqualTo("Start after retry")
assertThat(openRequest.generationSettings).isEqualTo(request.generationSettings)
@@ -547,7 +696,7 @@ class AgentSessionLaunchServiceTest {
val duplicateResult = handle.launch(request)
assertThat(duplicateResult.launched).isFalse()
assertThat(duplicateResult.error).isEqualTo(AgentPromptLaunchError.DROPPED_DUPLICATE)
assertThat(chatOpenExecutor.openNewChatCalls.get()).isEqualTo(1)
assertThat(threadViewOpenExecutor.openNewThreadViewCalls.get()).isEqualTo(1)
assertThat(launchSpecAttempts.get()).isEqualTo(1)
}
}
@@ -561,7 +710,7 @@ class AgentSessionLaunchServiceTest {
unarchiveThreadHandler = { _, _ -> false },
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
val pendingThread = thread(id = "new-global-prompt", updatedAt = 200, provider = AgentSessionProvider.from("codex"))
AgentSessionProviders.withRegistryForTest(InMemoryAgentSessionProviderRegistry(listOf(descriptor))) {
@@ -569,7 +718,7 @@ class AgentSessionLaunchServiceTest {
withTestServiceAndLaunch(
sessionSourcesProvider = { listOf(sourceForActiveThreads(listOf(pendingThread))) },
projectEntriesProvider = { listOf(openTestProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { service, launchService ->
service.refresh()
waitForCondition { activeThreadIds(service.state.value).contains(pendingThread.id) }
@@ -586,8 +735,8 @@ class AgentSessionLaunchServiceTest {
assertThat(result.launched).isFalse()
assertThat(result.error).isEqualTo(AgentPromptLaunchError.TARGET_THREAD_NOT_FOUND)
assertThat(chatOpenExecutor.openChatCalls.get()).isZero()
assertThat(chatOpenExecutor.openNewChatCalls.get()).isZero()
assertThat(threadViewOpenExecutor.openThreadViewCalls.get()).isZero()
assertThat(threadViewOpenExecutor.openNewThreadViewCalls.get()).isZero()
}
}
}
@@ -602,14 +751,14 @@ class AgentSessionLaunchServiceTest {
supportsPromptLaunch = false,
)
val activeThread = thread(id = "codex-active", updatedAt = 200, provider = AgentSessionProvider.from("codex"))
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(InMemoryAgentSessionProviderRegistry(listOf(descriptor))) {
runBlocking(Dispatchers.Default) {
withTestServiceAndLaunch(
sessionSourcesProvider = { listOf(sourceForActiveThreads(listOf(activeThread))) },
projectEntriesProvider = { listOf(openTestProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { service, launchService ->
service.refresh()
waitForCondition { activeThreadIds(service.state.value).contains(activeThread.id) }
@@ -636,8 +785,8 @@ class AgentSessionLaunchServiceTest {
assertThat(existingThreadResult.error).isEqualTo(AgentPromptLaunchError.PROVIDER_UNAVAILABLE)
assertThat(newThreadResult.launched).isFalse()
assertThat(newThreadResult.error).isEqualTo(AgentPromptLaunchError.PROVIDER_UNAVAILABLE)
assertThat(chatOpenExecutor.openChatCalls.get()).isZero()
assertThat(chatOpenExecutor.openNewChatCalls.get()).isZero()
assertThat(threadViewOpenExecutor.openThreadViewCalls.get()).isZero()
assertThat(threadViewOpenExecutor.openNewThreadViewCalls.get()).isZero()
}
}
}
@@ -651,7 +800,7 @@ class AgentSessionLaunchServiceTest {
cliAvailable = true,
supportsPromptLaunch = false,
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
val uiPreferencesState = AgentSessionUiPreferencesStateService()
AgentSessionProviders.withRegistryForTest(InMemoryAgentSessionProviderRegistry(listOf(descriptor))) {
@@ -660,7 +809,7 @@ class AgentSessionLaunchServiceTest {
sessionSourcesProvider = { listOf(descriptor.sessionSource) },
projectEntriesProvider = { listOf(openTestProjectEntry(PROJECT_PATH, "Project A")) },
uiPreferencesState = uiPreferencesState,
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { _, launchService ->
launchService.createNewSession(
path = PROJECT_PATH,
@@ -668,7 +817,7 @@ class AgentSessionLaunchServiceTest {
entryPoint = AgentWorkbenchEntryPoint.TREE_ROW,
)
waitForCondition { chatOpenExecutor.openNewChatCalls.get() == 1 }
waitForCondition { threadViewOpenExecutor.openNewThreadViewCalls.get() == 1 }
assertThat(uiPreferencesState.getProviderPreferences().providerOptionsByProviderId).isEmpty()
}
@@ -692,14 +841,14 @@ class AgentSessionLaunchServiceTest {
)
},
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(InMemoryAgentSessionProviderRegistry(listOf(descriptor))) {
runBlocking(Dispatchers.Default) {
withTestServiceAndLaunch(
sessionSourcesProvider = { listOf(ScriptedSessionSource(provider = provider)) },
projectEntriesProvider = { listOf(openTestProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { _, launchService ->
launchService.createNewSession(
path = PROJECT_PATH,
@@ -707,9 +856,9 @@ class AgentSessionLaunchServiceTest {
entryPoint = AgentWorkbenchEntryPoint.TREE_POPUP,
)
waitForCondition { chatOpenExecutor.openNewChatCalls.get() == 1 }
waitForCondition { threadViewOpenExecutor.openNewThreadViewCalls.get() == 1 }
val openRequest = checkNotNull(chatOpenExecutor.lastOpenNewChatRequest.get())
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenNewThreadViewRequest.get())
assertThat(openRequest.identity).isEqualTo(buildAgentSessionIdentity(provider, preallocatedSessionId))
assertThat(openRequest.launchSpec.preallocatedSessionId).isEqualTo(preallocatedSessionId)
}
@@ -717,6 +866,57 @@ class AgentSessionLaunchServiceTest {
}
}
@Test
fun createNewSessionBuilderAndPreparedHandlerUsePreallocatedLaunchSpecSessionId() {
val provider = AgentSessionProvider.from("pi")
val preallocatedSessionId = "f174b4df-e942-49fe-bb30-8b5f8e7f4857"
val builderThreadIds = CopyOnWriteArrayList<String>()
val preparedThreadIds = CopyOnWriteArrayList<String>()
val descriptor = TestAgentSessionProviderDescriptor(
provider = provider,
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
cliAvailable = true,
newSessionLaunchSpecProvider = {
AgentSessionTerminalLaunchSpec(
command = listOf("test", "new"),
preallocatedSessionId = preallocatedSessionId,
)
},
)
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(InMemoryAgentSessionProviderRegistry(listOf(descriptor))) {
runBlocking(Dispatchers.Default) {
withTestServiceAndLaunch(
sessionSourcesProvider = { listOf(ScriptedSessionSource(provider = provider)) },
projectEntriesProvider = { listOf(openTestProjectEntry(PROJECT_PATH, "Project A")) },
threadViewOpenExecutor = threadViewOpenExecutor,
) { _, launchService ->
launchService.createNewSession(
path = PROJECT_PATH,
provider = provider,
entryPoint = AgentWorkbenchEntryPoint.TREE_POPUP,
initialMessageRequestBuilder = { context ->
builderThreadIds += context.threadId
AgentPromptInitialMessageRequest(prompt = "Start task folder ${context.threadId}")
},
preparedLaunchHandler = { context ->
preparedThreadIds += context.threadId
},
)
waitForCondition { threadViewOpenExecutor.openNewThreadViewCalls.get() == 1 }
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenNewThreadViewRequest.get())
assertThat(openRequest.identity).isEqualTo(buildAgentSessionIdentity(provider, preallocatedSessionId))
assertThat(openRequest.initialComposedMessage).isEqualTo("Start task folder $preallocatedSessionId")
assertThat(builderThreadIds).containsExactly(preallocatedSessionId)
assertThat(preparedThreadIds).containsExactly(preallocatedSessionId)
}
}
}
}
@Test
fun openCodexThreadRebindsMatchingPendingTabBeforeOpening() {
assertOpenThreadRebindsMatchingPendingTabBeforeOpening(AgentSessionProvider.from("codex"))
@@ -738,25 +938,25 @@ class AgentSessionLaunchServiceTest {
false
},
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(InMemoryAgentSessionProviderRegistry(listOf(descriptor))) {
runBlocking(Dispatchers.Default) {
withTestServiceAndLaunch(
sessionSourcesProvider = { listOf(sourceForActiveThreads(emptyList())) },
projectEntriesProvider = { listOf(openTestProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
archivedSessionsRefreshIfLoaded = { archivedRefreshCalls.incrementAndGet() },
) { _, launchService ->
launchService.openChatThread(
launchService.openThreadViewThread(
path = PROJECT_PATH,
thread = thread(id = "codex-archived", updatedAt = 200, provider = AgentSessionProvider.from("codex")).copy(archived = true),
entryPoint = AgentWorkbenchEntryPoint.TREE_ROW,
)
waitForCondition { chatOpenExecutor.openChatCalls.get() == 1 }
waitForCondition { threadViewOpenExecutor.openThreadViewCalls.get() == 1 }
val openRequest = checkNotNull(chatOpenExecutor.lastOpenChatRequest.get())
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenThreadViewRequest.get())
assertThat(openRequest.thread.id).isEqualTo("codex-archived")
assertThat(openRequest.thread.archived).isTrue()
assertThat(unarchiveCalls.get()).isEqualTo(1)
@@ -777,25 +977,25 @@ class AgentSessionLaunchServiceTest {
true
},
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(InMemoryAgentSessionProviderRegistry(listOf(descriptor))) {
runBlocking(Dispatchers.Default) {
withTestServiceAndLaunch(
sessionSourcesProvider = { listOf(sourceForActiveThreads(emptyList())) },
projectEntriesProvider = { listOf(openTestProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
archivedSessionsRefreshIfLoaded = { archivedRefreshCalls.incrementAndGet() },
) { _, launchService ->
launchService.openChatThread(
launchService.openThreadViewThread(
path = PROJECT_PATH,
thread = thread(id = "codex-archived", updatedAt = 200, provider = AgentSessionProvider.from("codex")).copy(archived = true),
entryPoint = AgentWorkbenchEntryPoint.TREE_ROW,
)
waitForCondition { chatOpenExecutor.openChatCalls.get() == 1 }
waitForCondition { threadViewOpenExecutor.openThreadViewCalls.get() == 1 }
val openRequest = checkNotNull(chatOpenExecutor.lastOpenChatRequest.get())
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenThreadViewRequest.get())
assertThat(openRequest.thread.id).isEqualTo("codex-archived")
assertThat(openRequest.thread.archived).isTrue()
assertThat(unarchiveCalls.get()).isZero()
@@ -829,7 +1029,7 @@ class AgentSessionLaunchServiceTest {
true
},
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
val sessionSource = sourceForActiveThreads(activeThreads)
AgentSessionProviders.withRegistryForTest(InMemoryAgentSessionProviderRegistry(listOf(descriptor))) {
@@ -837,7 +1037,7 @@ class AgentSessionLaunchServiceTest {
withServiceAndArchiveAndLaunch(
sessionSourcesProvider = { listOf(sessionSource) },
projectEntriesProvider = { listOf(openProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
archivedSessionsRefreshIfLoaded = { archivedRefreshCalls.incrementAndGet() },
) { service, archiveService, launchService ->
service.refresh()
@@ -854,19 +1054,19 @@ class AgentSessionLaunchServiceTest {
!activeThreadIds(service.state.value).contains(activeThread.id)
}
launchService.openChatThread(
launchService.openThreadViewThread(
path = PROJECT_PATH,
thread = activeThread.copy(archived = true),
entryPoint = AgentWorkbenchEntryPoint.TREE_ROW,
)
waitForCondition {
chatOpenExecutor.openChatCalls.get() == 1 &&
threadViewOpenExecutor.openThreadViewCalls.get() == 1 &&
archivedRefreshCalls.get() == 1 &&
activeThreadIds(service.state.value).contains(activeThread.id)
}
val openRequest = checkNotNull(chatOpenExecutor.lastOpenChatRequest.get())
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenThreadViewRequest.get())
assertThat(openRequest.thread.id).isEqualTo(activeThread.id)
assertThat(openRequest.thread.archived).isFalse()
assertThat(archiveCalls.get()).isEqualTo(1)
@@ -896,7 +1096,7 @@ class AgentSessionLaunchServiceTest {
threadId == archivedThread.id
},
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
val sessionSource = sourceForActiveThreads(activeThreads)
AgentSessionProviders.withRegistryForTest(InMemoryAgentSessionProviderRegistry(listOf(descriptor))) {
@@ -904,10 +1104,10 @@ class AgentSessionLaunchServiceTest {
withTestServiceAndLaunch(
sessionSourcesProvider = { listOf(sessionSource) },
projectEntriesProvider = { listOf(openTestProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
archivedSessionsRefreshIfLoaded = { archivedRefreshCalls.incrementAndGet() },
) { service, launchService ->
launchService.openChatSubAgent(
launchService.openThreadViewSubAgent(
path = PROJECT_PATH,
thread = archivedThread,
subAgent = subAgent,
@@ -915,12 +1115,12 @@ class AgentSessionLaunchServiceTest {
)
waitForCondition {
chatOpenExecutor.openChatCalls.get() == 1 &&
threadViewOpenExecutor.openThreadViewCalls.get() == 1 &&
archivedRefreshCalls.get() == 1 &&
activeThreadIds(service.state.value).contains("codex-archived")
}
val openRequest = checkNotNull(chatOpenExecutor.lastOpenChatRequest.get())
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenThreadViewRequest.get())
assertThat(openRequest.thread.id).isEqualTo("codex-archived")
assertThat(openRequest.thread.archived).isFalse()
assertThat(openRequest.subAgent).isEqualTo(subAgent)
@@ -945,7 +1145,7 @@ class AgentSessionLaunchServiceTest {
title = "Resolved open",
provider = provider,
)
val pendingTab = AgentChatPendingTabSnapshot(
val pendingTab = AgentThreadViewPendingTabSnapshot(
projectPath = PROJECT_PATH,
pendingTabKey = "pending-$pendingThreadIdentity",
pendingThreadIdentity = pendingThreadIdentity,
@@ -953,9 +1153,9 @@ class AgentSessionLaunchServiceTest {
pendingFirstInputAtMs = null,
pendingLaunchMode = "standard",
)
val rebindInvocations = CopyOnWriteArrayList<AgentChatPendingTabRebindRequest>()
val chatOpenExecutor = RecordingChatOpenExecutor(
onOpenChat = { _, _ ->
val rebindInvocations = CopyOnWriteArrayList<AgentThreadViewPendingTabRebindRequest>()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor(
onOpenThreadView = { _, _ ->
assertThat(rebindInvocations).hasSize(1)
}
)
@@ -965,23 +1165,23 @@ class AgentSessionLaunchServiceTest {
withTestServiceAndLaunch(
sessionSourcesProvider = { listOf(ScriptedSessionSource(provider = provider)) },
projectEntriesProvider = { listOf(openTestProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
openPendingAgentChatTabsProvider = { requestedProvider ->
threadViewOpenExecutor = threadViewOpenExecutor,
openPendingAgentThreadViewTabsProvider = { requestedProvider ->
if (requestedProvider == provider) mapOf(PROJECT_PATH to listOf(pendingTab)) else emptyMap()
},
openAgentChatPendingTabsBinderWithProvider = { requestedProvider, requestsByPath ->
openAgentThreadViewPendingTabsBinderWithProvider = { requestedProvider, requestsByPath ->
assertThat(requestedProvider.value).isEqualTo(provider.value)
requestsByPath.values.flatten().forEach(rebindInvocations::add)
successfulPendingRebindReport(requestsByPath)
},
) { _, launchService ->
launchService.openChatThread(
launchService.openThreadViewThread(
path = PROJECT_PATH,
thread = resolvedThread,
entryPoint = AgentWorkbenchEntryPoint.TREE_ROW,
)
waitForCondition { chatOpenExecutor.openChatCalls.get() == 1 }
waitForCondition { threadViewOpenExecutor.openThreadViewCalls.get() == 1 }
val rebindRequest = rebindInvocations.single()
assertThat(rebindRequest.pendingTabKey).isEqualTo("pending-$pendingThreadIdentity")
@@ -989,7 +1189,7 @@ class AgentSessionLaunchServiceTest {
assertThat(rebindRequest.target.provider).isEqualTo(provider)
assertThat(rebindRequest.target.threadIdentity).isEqualTo(buildAgentSessionIdentity(provider, resolvedThread.id))
assertThat(rebindRequest.target.threadId).isEqualTo(resolvedThread.id)
assertThat(checkNotNull(chatOpenExecutor.lastOpenChatRequest.get()).thread.id).isEqualTo(resolvedThread.id)
assertThat(checkNotNull(threadViewOpenExecutor.lastOpenThreadViewRequest.get()).thread.id).isEqualTo(resolvedThread.id)
}
}
}
@@ -1062,22 +1262,22 @@ private fun uiPreferencesStateWithProfiles(vararg profiles: AgentPromptLaunchPro
}
private fun successfulPendingRebindReport(
requestsByPath: Map<String, List<AgentChatPendingTabRebindRequest>>,
): AgentChatPendingTabRebindReport {
val outcomesByPath = LinkedHashMap<String, List<AgentChatPendingTabRebindOutcome>>()
requestsByPath: Map<String, List<AgentThreadViewPendingTabRebindRequest>>,
): AgentThreadViewPendingTabRebindReport {
val outcomesByPath = LinkedHashMap<String, List<AgentThreadViewPendingTabRebindOutcome>>()
var requestedBindings = 0
for ((path, requests) in requestsByPath) {
requestedBindings += requests.size
outcomesByPath[path] = requests.map { request ->
AgentChatPendingTabRebindOutcome(
AgentThreadViewPendingTabRebindOutcome(
projectPath = path,
request = request,
status = AgentChatPendingTabRebindStatus.REBOUND,
status = AgentThreadViewPendingTabRebindStatus.REBOUND,
reboundFiles = 1,
)
}
}
return AgentChatPendingTabRebindReport(
return AgentThreadViewPendingTabRebindReport(
requestedBindings = requestedBindings,
reboundBindings = requestedBindings,
reboundFiles = requestedBindings,
@@ -18,7 +18,7 @@ import com.intellij.agent.workbench.prompt.core.AgentPromptLauncherBridge
import com.intellij.agent.workbench.prompt.core.AgentPromptProjectPathCandidate
import com.intellij.agent.workbench.prompt.core.AgentPromptProjectPathContext
import com.intellij.agent.workbench.prompt.core.AgentPromptReasoningEffort
import com.intellij.agent.workbench.sessions.frame.AgentChatOpenModeSettings
import com.intellij.agent.workbench.sessions.frame.AgentThreadViewOpenModeSettings
import com.intellij.agent.workbench.sessions.frame.AgentWorkbenchDedicatedFrameProjectManager
import com.intellij.agent.workbench.sessions.model.AgentSessionProviderLoadState
import com.intellij.agent.workbench.sessions.service.AgentSessionLaunchService
@@ -96,7 +96,7 @@ class AgentSessionPromptLauncherBridgeTest {
provider = AgentSessionProvider.from("codex"),
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(
InMemoryAgentSessionProviderRegistry(listOf(providerBridge))
) {
@@ -104,7 +104,7 @@ class AgentSessionPromptLauncherBridgeTest {
withServiceAndLaunch(
sessionSourcesProvider = { listOf(providerBridge.sessionSource) },
projectEntriesProvider = { listOf(openProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { service, launchService ->
val bridge = promptLauncherBridge(service, launchService)
val request = promptLaunchRequest(projectPath = INVALID_PROMPT_PROJECT_PATH)
@@ -120,7 +120,7 @@ class AgentSessionPromptLauncherBridgeTest {
providerBridge.createCalls.get() == 1 &&
providerBridge.composeCalls.get() == 1 &&
providerBridge.startupCommandCalls.get() == 1 &&
chatOpenExecutor.openNewChatCalls.get() == 1 &&
threadViewOpenExecutor.openNewThreadViewCalls.get() == 1 &&
telemetryEvents.any { it.id == AgentWorkbenchTelemetry.PROMPT_LAUNCH_RESOLVED_EVENT_ID }
}
@@ -131,7 +131,7 @@ class AgentSessionPromptLauncherBridgeTest {
assertThat(providerBridge.lastStartupBaseLaunchSpec.get()?.command)
.containsExactly("test", "new", AgentSessionLaunchMode.STANDARD.name)
assertThat(providerBridge.lastStartupPrompt.get()).isEqualTo("composed:Refactor selected code")
val openRequest = checkNotNull(chatOpenExecutor.lastOpenNewChatRequest.get())
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenNewThreadViewRequest.get())
assertThat(openRequest.normalizedPath).isEqualTo(INVALID_PROMPT_PROJECT_PATH)
assertThat(openRequest.identity).startsWith("codex:new-")
assertThat(openRequest.launchSpec.command)
@@ -471,8 +471,8 @@ class AgentSessionPromptLauncherBridgeTest {
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
)
val releaseFirstOpen = CompletableDeferred<Unit>()
val chatOpenExecutor = RecordingChatOpenExecutor(
onOpenNewChat = { _, callIndex ->
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor(
onOpenNewThreadView = { _, callIndex ->
if (callIndex == 1) {
releaseFirstOpen.await()
}
@@ -486,7 +486,7 @@ class AgentSessionPromptLauncherBridgeTest {
withServiceAndLaunch(
sessionSourcesProvider = { listOf(providerBridge.sessionSource) },
projectEntriesProvider = { listOf(openProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { service, launchService ->
val bridge = promptLauncherBridge(service, launchService)
val telemetryEvents = CopyOnWriteArrayList<AgentWorkbenchTelemetryEvent>()
@@ -495,7 +495,7 @@ class AgentSessionPromptLauncherBridgeTest {
try {
val firstResult = bridge.launch(promptLaunchRequest(projectPath = INVALID_PROMPT_PROJECT_PATH))
waitForCondition {
chatOpenExecutor.openNewChatCalls.get() == 1
threadViewOpenExecutor.openNewThreadViewCalls.get() == 1
}
val secondResult = bridge.launch(promptLaunchRequest(projectPath = INVALID_PROMPT_PROJECT_PATH))
@@ -540,8 +540,8 @@ class AgentSessionPromptLauncherBridgeTest {
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
)
val releaseFirstOpen = CompletableDeferred<Unit>()
val chatOpenExecutor = RecordingChatOpenExecutor(
onOpenNewChat = { _, callIndex ->
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor(
onOpenNewThreadView = { _, callIndex ->
if (callIndex == 1) {
releaseFirstOpen.await()
}
@@ -555,7 +555,7 @@ class AgentSessionPromptLauncherBridgeTest {
withServiceAndLaunch(
sessionSourcesProvider = { listOf(providerBridge.sessionSource) },
projectEntriesProvider = { listOf(openProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { _, launchService ->
try {
launchService.createNewSession(
@@ -566,7 +566,7 @@ class AgentSessionPromptLauncherBridgeTest {
singleFlightDiscriminator = "merge-session-1",
)
waitForCondition {
chatOpenExecutor.openNewChatCalls.get() == 1
threadViewOpenExecutor.openNewThreadViewCalls.get() == 1
}
launchService.createNewSession(
@@ -579,7 +579,7 @@ class AgentSessionPromptLauncherBridgeTest {
waitForCondition {
providerBridge.createCalls.get() == 2 &&
chatOpenExecutor.openNewChatCalls.get() == 2
threadViewOpenExecutor.openNewThreadViewCalls.get() == 2
}
}
finally {
@@ -592,15 +592,15 @@ class AgentSessionPromptLauncherBridgeTest {
}
@Test
fun createNewSessionWaitsForOpenedChatHandlerBeforeReleasingSingleFlight() {
fun createNewSessionWaitsForOpenedThreadViewHandlerBeforeReleasingSingleFlight() {
val providerBridge = RecordingPromptLaunchProviderBridge(
provider = AgentSessionProvider.from("codex"),
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
)
val releaseOpenedChatHandler = CompletableDeferred<Unit>()
val releaseOpenedThreadViewHandler = CompletableDeferred<Unit>()
val firstLaunchResult = CompletableDeferred<AgentPromptLaunchResult>()
val secondLaunchResult = CompletableDeferred<AgentPromptLaunchResult>()
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
withOpenInNonDedicatedFrameSettingForTest {
AgentSessionProviders.withRegistryForTest(
InMemoryAgentSessionProviderRegistry(listOf(providerBridge))
@@ -609,17 +609,17 @@ class AgentSessionPromptLauncherBridgeTest {
withServiceAndLaunch(
sessionSourcesProvider = { listOf(providerBridge.sessionSource) },
projectEntriesProvider = { listOf(openProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { _, launchService ->
launchService.createNewSession(
path = PROJECT_PATH,
provider = AgentSessionProvider.from("codex"),
mode = AgentSessionLaunchMode.STANDARD,
entryPoint = AgentWorkbenchEntryPoint.TOOLBAR,
openedChatHandler = { _, _ -> releaseOpenedChatHandler.await() },
openedThreadViewHandler = { _, _ -> releaseOpenedThreadViewHandler.await() },
promptLaunchResolved = { result -> firstLaunchResult.complete(result) },
)
chatOpenExecutor.awaitOpenPreparingNewChatCalls(1)
threadViewOpenExecutor.awaitOpenPreparingNewThreadViewCalls(1)
launchService.createNewSession(
path = PROJECT_PATH,
@@ -636,12 +636,12 @@ class AgentSessionPromptLauncherBridgeTest {
assertThat(firstLaunchResult.isCompleted).isFalse()
assertThat(droppedDuplicateResult.launched).isFalse()
assertThat(droppedDuplicateResult.error).isEqualTo(AgentPromptLaunchError.DROPPED_DUPLICATE)
assertThat(chatOpenExecutor.openNewChatCalls.get()).isZero()
assertThat(threadViewOpenExecutor.openNewThreadViewCalls.get()).isZero()
releaseOpenedChatHandler.complete(Unit)
releaseOpenedThreadViewHandler.complete(Unit)
assertThat(firstLaunchResult.await()).isEqualTo(AgentPromptLaunchResult.SUCCESS)
chatOpenExecutor.awaitOpenNewChatCalls(1)
threadViewOpenExecutor.awaitOpenNewThreadViewCalls(1)
}
}
}
@@ -649,7 +649,7 @@ class AgentSessionPromptLauncherBridgeTest {
}
@Test
fun createNewSessionRunsOpenAndOpenedChatHandlerInProvidedLaunchModality() {
fun createNewSessionRunsOpenAndOpenedThreadViewHandlerInProvidedLaunchModality() {
val providerBridge = RecordingPromptLaunchProviderBridge(
provider = AgentSessionProvider.from("codex"),
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
@@ -657,8 +657,8 @@ class AgentSessionPromptLauncherBridgeTest {
val openModality = AtomicReference<ModalityState?>(null)
val handlerModality = AtomicReference<ModalityState?>(null)
val launchModalityState = createTestModalityState()
val chatOpenExecutor = RecordingChatOpenExecutor(
onOpenNewChat = { _, _ ->
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor(
onOpenNewThreadView = { _, _ ->
openModality.set(currentCoroutineContext().contextModality())
}
)
@@ -670,7 +670,7 @@ class AgentSessionPromptLauncherBridgeTest {
withServiceAndLaunch(
sessionSourcesProvider = { listOf(providerBridge.sessionSource) },
projectEntriesProvider = { listOf(openProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { _, launchService ->
assertThat(launchModalityState).isNotEqualTo(ModalityState.nonModal())
@@ -680,7 +680,7 @@ class AgentSessionPromptLauncherBridgeTest {
mode = AgentSessionLaunchMode.STANDARD,
entryPoint = AgentWorkbenchEntryPoint.TOOLBAR,
launchModalityState = launchModalityState,
openedChatHandler = { _, _ ->
openedThreadViewHandler = { _, _ ->
handlerModality.set(currentCoroutineContext().contextModality())
},
)
@@ -690,7 +690,7 @@ class AgentSessionPromptLauncherBridgeTest {
handlerModality.get() === launchModalityState
}
assertThat(chatOpenExecutor.openNewChatCalls.get()).isEqualTo(1)
assertThat(threadViewOpenExecutor.openNewThreadViewCalls.get()).isEqualTo(1)
}
}
}
@@ -703,7 +703,7 @@ class AgentSessionPromptLauncherBridgeTest {
provider = AgentSessionProvider.from("claude"),
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
val uiPreferencesState = AgentSessionUiPreferencesStateService()
AgentSessionProviders.withRegistryForTest(
InMemoryAgentSessionProviderRegistry(listOf(providerBridge))
@@ -713,7 +713,7 @@ class AgentSessionPromptLauncherBridgeTest {
sessionSourcesProvider = { listOf(providerBridge.sessionSource) },
projectEntriesProvider = { listOf(openProjectEntry(PROJECT_PATH, "Project A")) },
uiPreferencesState = uiPreferencesState,
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { service, launchService ->
val bridge = promptLauncherBridge(service, launchService)
val request = promptLaunchRequest(provider = AgentSessionProvider.from("claude"))
@@ -746,7 +746,7 @@ class AgentSessionPromptLauncherBridgeTest {
launchSpec.copy(command = launchSpec.command + listOf("--effort", settings.reasoningEffort.name.lowercase()))
},
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(
InMemoryAgentSessionProviderRegistry(listOf(providerBridge))
) {
@@ -754,7 +754,7 @@ class AgentSessionPromptLauncherBridgeTest {
withServiceAndLaunch(
sessionSourcesProvider = { listOf(providerBridge.sessionSource) },
projectEntriesProvider = { listOf(openProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { service, launchService ->
val bridge = promptLauncherBridge(service, launchService)
@@ -766,10 +766,10 @@ class AgentSessionPromptLauncherBridgeTest {
)
assertThat(result.launched).isTrue()
waitForCondition { chatOpenExecutor.openNewChatCalls.get() == 1 }
assertThat(chatOpenExecutor.lastOpenNewChatRequest.get()?.launchSpec?.command)
waitForCondition { threadViewOpenExecutor.openNewThreadViewCalls.get() == 1 }
assertThat(threadViewOpenExecutor.lastOpenNewThreadViewRequest.get()?.launchSpec?.command)
.containsExactly("test", "new", AgentSessionLaunchMode.STANDARD.name, "--effort", "high")
assertThat(chatOpenExecutor.lastOpenNewChatRequest.get()?.generationSettings)
assertThat(threadViewOpenExecutor.lastOpenNewThreadViewRequest.get()?.generationSettings)
.isEqualTo(AgentPromptGenerationSettings(reasoningEffort = AgentPromptReasoningEffort.HIGH))
}
}
@@ -787,7 +787,7 @@ class AgentSessionPromptLauncherBridgeTest {
launchSpec.copy(command = launchSpec.command + listOf("--model", settings.modelId.orEmpty()))
},
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(
InMemoryAgentSessionProviderRegistry(listOf(providerBridge))
) {
@@ -809,7 +809,7 @@ class AgentSessionPromptLauncherBridgeTest {
)
},
projectEntriesProvider = { listOf(openProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { service, launchService ->
service.refresh()
waitForCondition {
@@ -829,13 +829,13 @@ class AgentSessionPromptLauncherBridgeTest {
)
assertThat(result.launched).isTrue()
waitForCondition { chatOpenExecutor.openChatCalls.get() == 1 }
val openRequest = chatOpenExecutor.lastOpenChatRequest.get()
waitForCondition { threadViewOpenExecutor.openThreadViewCalls.get() == 1 }
val openRequest = threadViewOpenExecutor.lastOpenThreadViewRequest.get()
assertThat(openRequest?.launchSpecOverride?.command)
.containsExactly("test", "resume", "thread-existing", "--model", "pi:custom-model")
assertThat(openRequest?.generationSettings).isEqualTo(generationSettings)
assertThat(providerBridge.lastGenerationSettings.get()).isEqualTo(generationSettings)
assertThat(chatOpenExecutor.openNewChatCalls.get()).isZero()
assertThat(threadViewOpenExecutor.openNewThreadViewCalls.get()).isZero()
}
}
}
@@ -847,7 +847,7 @@ class AgentSessionPromptLauncherBridgeTest {
provider = AgentSessionProvider.from("codex"),
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
val uiPreferencesState = AgentSessionUiPreferencesStateService().apply {
setProviderPreferences(
AgentPromptLauncherBridge.ProviderPreferences(
@@ -863,7 +863,7 @@ class AgentSessionPromptLauncherBridgeTest {
sessionSourcesProvider = { listOf(providerBridge.sessionSource) },
projectEntriesProvider = { listOf(openProjectEntry(PROJECT_PATH, "Project A")) },
uiPreferencesState = uiPreferencesState,
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { _, launchService ->
launchService.createNewSession(
path = PROJECT_PATH,
@@ -874,7 +874,7 @@ class AgentSessionPromptLauncherBridgeTest {
)
waitForCondition {
chatOpenExecutor.openNewChatCalls.get() == 1
threadViewOpenExecutor.openNewThreadViewCalls.get() == 1
}
assertThat(uiPreferencesState.getProviderPreferences().providerOptionsByProviderId)
.isEqualTo(mapOf("claude" to setOf("plan_mode")))
@@ -890,7 +890,7 @@ class AgentSessionPromptLauncherBridgeTest {
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
startupPromptCommandEnvVariables = mapOf("DISABLE_AUTOUPDATER" to "1"),
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(
InMemoryAgentSessionProviderRegistry(listOf(providerBridge))
) {
@@ -898,7 +898,7 @@ class AgentSessionPromptLauncherBridgeTest {
withServiceAndLaunch(
sessionSourcesProvider = { listOf(providerBridge.sessionSource) },
projectEntriesProvider = { listOf(openProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { service, launchService ->
val bridge = promptLauncherBridge(service, launchService)
val result = bridge.launch(promptLaunchRequest(projectPath = INVALID_PROMPT_PROJECT_PATH))
@@ -906,9 +906,9 @@ class AgentSessionPromptLauncherBridgeTest {
assertThat(result.launched).isTrue()
assertThat(result.error).isNull()
waitForCondition {
chatOpenExecutor.openNewChatCalls.get() == 1
threadViewOpenExecutor.openNewThreadViewCalls.get() == 1
}
val openRequest = checkNotNull(chatOpenExecutor.lastOpenNewChatRequest.get())
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenNewThreadViewRequest.get())
assertThat(openRequest.startupLaunchSpecOverride?.envVariables)
.containsExactlyEntriesOf(mapOf("DISABLE_AUTOUPDATER" to "1"))
}
@@ -923,7 +923,7 @@ class AgentSessionPromptLauncherBridgeTest {
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
startupPromptCommandEnvVariables = mapOf("DISABLE_AUTOUPDATER" to "1"),
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(
InMemoryAgentSessionProviderRegistry(listOf(providerBridge))
) {
@@ -932,7 +932,7 @@ class AgentSessionPromptLauncherBridgeTest {
withServiceAndLaunch(
sessionSourcesProvider = { listOf(providerBridge.sessionSource) },
projectEntriesProvider = { listOf(openProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { service, launchService ->
val bridge = promptLauncherBridge(service, launchService)
val result = bridge.launch(promptLaunchRequest(projectPath = PROJECT_PATH))
@@ -940,10 +940,10 @@ class AgentSessionPromptLauncherBridgeTest {
assertThat(result.launched).isTrue()
assertThat(result.error).isNull()
waitForCondition {
chatOpenExecutor.openNewChatCalls.get() == 1
threadViewOpenExecutor.openNewThreadViewCalls.get() == 1
}
val openRequest = checkNotNull(chatOpenExecutor.lastOpenNewChatRequest.get())
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenNewThreadViewRequest.get())
assertThat(openRequest.launchSpec.envVariables)
.containsEntry(AGENT_WORKBENCH_TEST_ENV_NAME, AGENT_WORKBENCH_TEST_ENV_VALUE)
.containsEntry("PATH", AGENT_WORKBENCH_TEST_PATH_PREPEND)
@@ -964,7 +964,7 @@ class AgentSessionPromptLauncherBridgeTest {
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
startupPromptCommandSupported = false,
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(
InMemoryAgentSessionProviderRegistry(listOf(providerBridge))
) {
@@ -972,7 +972,7 @@ class AgentSessionPromptLauncherBridgeTest {
withServiceAndLaunch(
sessionSourcesProvider = { listOf(providerBridge.sessionSource) },
projectEntriesProvider = { listOf(openProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { service, launchService ->
val bridge = promptLauncherBridge(service, launchService)
val result = bridge.launch(promptLaunchRequest(projectPath = INVALID_PROMPT_PROJECT_PATH))
@@ -988,9 +988,9 @@ class AgentSessionPromptLauncherBridgeTest {
assertThat(providerBridge.startupCommandCalls.get()).isEqualTo(1)
assertThat(providerBridge.lastStartupPrompt.get()).isEqualTo("composed:Refactor selected code")
waitForCondition {
chatOpenExecutor.openNewChatCalls.get() == 1
threadViewOpenExecutor.openNewThreadViewCalls.get() == 1
}
val openRequest = checkNotNull(chatOpenExecutor.lastOpenNewChatRequest.get())
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenNewThreadViewRequest.get())
assertThat(openRequest.startupLaunchSpecOverride).isNull()
assertThat(openRequest.initialComposedMessage).isEqualTo("composed:Refactor selected code")
assertThat(openRequest.initialMessageToken).isNotNull()
@@ -1006,7 +1006,7 @@ class AgentSessionPromptLauncherBridgeTest {
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
startupPromptCommandPolicyEnabled = false,
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(
InMemoryAgentSessionProviderRegistry(listOf(providerBridge))
) {
@@ -1014,7 +1014,7 @@ class AgentSessionPromptLauncherBridgeTest {
withServiceAndLaunch(
sessionSourcesProvider = { listOf(providerBridge.sessionSource) },
projectEntriesProvider = { listOf(openProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { service, launchService ->
val bridge = promptLauncherBridge(service, launchService)
val request = promptLaunchRequest(projectPath = INVALID_PROMPT_PROJECT_PATH)
@@ -1036,9 +1036,9 @@ class AgentSessionPromptLauncherBridgeTest {
assertThat(providerBridge.startupCommandCalls.get()).isZero()
waitForCondition {
chatOpenExecutor.openNewChatCalls.get() == 1
threadViewOpenExecutor.openNewThreadViewCalls.get() == 1
}
val openRequest = checkNotNull(chatOpenExecutor.lastOpenNewChatRequest.get())
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenNewThreadViewRequest.get())
assertThat(openRequest.startupLaunchSpecOverride).isNull()
assertThat(openRequest.initialComposedMessage).isEqualTo("composed:Refactor selected code")
assertThat(openRequest.initialMessageToken).isNotNull()
@@ -1056,7 +1056,7 @@ class AgentSessionPromptLauncherBridgeTest {
composedMessageBuilder = { request -> request.prompt.trim() },
prestartPlanPromptThreadId = "thread-prestarted-plan",
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(
InMemoryAgentSessionProviderRegistry(listOf(providerBridge))
) {
@@ -1064,7 +1064,7 @@ class AgentSessionPromptLauncherBridgeTest {
withServiceAndLaunch(
sessionSourcesProvider = { listOf(providerBridge.sessionSource) },
projectEntriesProvider = { listOf(openProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { service, launchService ->
val bridge = promptLauncherBridge(service, launchService)
val baseRequest = promptLaunchRequest(projectPath = INVALID_PROMPT_PROJECT_PATH)
@@ -1082,7 +1082,7 @@ class AgentSessionPromptLauncherBridgeTest {
providerBridge.composeCalls.get() == 1 &&
providerBridge.createCalls.get() == 1 &&
providerBridge.prestartPlanPromptCalls.get() == 1 &&
chatOpenExecutor.openNewChatCalls.get() == 1
threadViewOpenExecutor.openNewThreadViewCalls.get() == 1
}
assertThat(providerBridge.createCalls.get()).isEqualTo(1)
@@ -1094,9 +1094,9 @@ class AgentSessionPromptLauncherBridgeTest {
assertThat(providerBridge.startupCommandCalls.get()).isZero()
assertThat(providerBridge.lastStartupBaseLaunchSpec.get()).isNull()
assertThat(providerBridge.lastStartupPrompt.get()).isNull()
assertThat(chatOpenExecutor.openChatCalls.get()).isZero()
assertThat(threadViewOpenExecutor.openThreadViewCalls.get()).isZero()
val openRequest = checkNotNull(chatOpenExecutor.lastOpenNewChatRequest.get())
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenNewThreadViewRequest.get())
assertThat(openRequest.identity).isEqualTo("codex:thread-prestarted-plan")
assertThat(openRequest.launchSpec.command)
.containsExactly(
@@ -1136,7 +1136,7 @@ class AgentSessionPromptLauncherBridgeTest {
composedMessageBuilder = { request -> request.prompt.trim() },
supportedReasoningEffortsOverride = setOf(AgentPromptReasoningEffort.HIGH),
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(
InMemoryAgentSessionProviderRegistry(listOf(providerBridge))
) {
@@ -1144,7 +1144,7 @@ class AgentSessionPromptLauncherBridgeTest {
withServiceAndLaunch(
sessionSourcesProvider = { listOf(providerBridge.sessionSource) },
projectEntriesProvider = { listOf(openProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { service, launchService ->
val bridge = promptLauncherBridge(service, launchService)
val baseRequest = promptLaunchRequest(projectPath = INVALID_PROMPT_PROJECT_PATH)
@@ -1159,7 +1159,7 @@ class AgentSessionPromptLauncherBridgeTest {
assertThat(result.error).isNull()
waitForCondition {
providerBridge.composeCalls.get() == 1 &&
chatOpenExecutor.openNewChatCalls.get() == 1
threadViewOpenExecutor.openNewThreadViewCalls.get() == 1
}
assertThat(providerBridge.createCalls.get()).isEqualTo(1)
@@ -1168,9 +1168,9 @@ class AgentSessionPromptLauncherBridgeTest {
assertThat(providerBridge.lastStartupBaseLaunchSpec.get()?.command)
.containsExactly("test", "new", AgentSessionLaunchMode.STANDARD.name)
assertThat(providerBridge.lastStartupPrompt.get()).isEqualTo("Refactor selected code")
assertThat(chatOpenExecutor.openChatCalls.get()).isZero()
assertThat(threadViewOpenExecutor.openThreadViewCalls.get()).isZero()
val openRequest = checkNotNull(chatOpenExecutor.lastOpenNewChatRequest.get())
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenNewThreadViewRequest.get())
assertThat(openRequest.launchSpec.command)
.containsExactly("test", "new", AgentSessionLaunchMode.STANDARD.name)
assertThat(openRequest.startupLaunchSpecOverride?.command)
@@ -1190,7 +1190,7 @@ class AgentSessionPromptLauncherBridgeTest {
provider = AgentSessionProvider.from("codex"),
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(
InMemoryAgentSessionProviderRegistry(listOf(providerBridge))
) {
@@ -1212,7 +1212,7 @@ class AgentSessionPromptLauncherBridgeTest {
)
},
projectEntriesProvider = { listOf(openProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { service, launchService ->
service.refresh()
waitForCondition {
@@ -1231,14 +1231,14 @@ class AgentSessionPromptLauncherBridgeTest {
assertThat(providerBridge.createCalls.get()).isZero()
waitForCondition {
providerBridge.composeCalls.get() == 1 &&
chatOpenExecutor.openChatCalls.get() == 1
threadViewOpenExecutor.openThreadViewCalls.get() == 1
}
assertThat(providerBridge.composeCalls.get()).isEqualTo(1)
assertThat(providerBridge.lastComposeRequest.get()).isEqualTo(request.initialMessageRequest)
assertThat(providerBridge.startupCommandCalls.get()).isZero()
assertThat(providerBridge.lastStartupBaseLaunchSpec.get()).isNull()
assertThat(providerBridge.lastStartupPrompt.get()).isNull()
val openRequest = checkNotNull(chatOpenExecutor.lastOpenChatRequest.get())
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenThreadViewRequest.get())
assertThat(openRequest.normalizedPath).isEqualTo(PROJECT_PATH)
assertThat(openRequest.thread.id).isEqualTo("thread-existing")
assertThat(openRequest.subAgent).isNull()
@@ -1248,7 +1248,7 @@ class AgentSessionPromptLauncherBridgeTest {
AgentInitialMessageDispatchStep(text = "composed:Refactor selected code"),
)
assertThat(openRequest.initialMessageToken).isNotNull()
assertThat(chatOpenExecutor.openNewChatCalls.get()).isZero()
assertThat(threadViewOpenExecutor.openNewThreadViewCalls.get()).isZero()
}
}
}
@@ -1267,7 +1267,7 @@ class AgentSessionPromptLauncherBridgeTest {
launchSpec.copy(command = launchSpec.command + listOf("--effort", settings.reasoningEffort.name.lowercase()))
},
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(
InMemoryAgentSessionProviderRegistry(listOf(providerBridge))
) {
@@ -1289,7 +1289,7 @@ class AgentSessionPromptLauncherBridgeTest {
)
},
projectEntriesProvider = { listOf(openProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { service, launchService ->
service.refresh()
waitForCondition {
@@ -1316,7 +1316,7 @@ class AgentSessionPromptLauncherBridgeTest {
assertThat(providerBridge.createCalls.get()).isZero()
waitForCondition {
providerBridge.composeCalls.get() == 1 &&
chatOpenExecutor.openChatCalls.get() == 1
threadViewOpenExecutor.openThreadViewCalls.get() == 1
}
assertThat(providerBridge.lastComposeRequest.get()).isEqualTo(request.initialMessageRequest)
@@ -1325,9 +1325,9 @@ class AgentSessionPromptLauncherBridgeTest {
.containsExactly("test", "resume", "thread-existing", "--effort", "high")
assertThat(providerBridge.lastStartupPrompt.get()).isEqualTo("Refactor selected code")
assertThat(providerBridge.generationSettingsApplyCalls.get()).isEqualTo(1)
assertThat(chatOpenExecutor.openNewChatCalls.get()).isZero()
assertThat(threadViewOpenExecutor.openNewThreadViewCalls.get()).isZero()
val openRequest = checkNotNull(chatOpenExecutor.lastOpenChatRequest.get())
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenThreadViewRequest.get())
assertThat(openRequest.normalizedPath).isEqualTo(PROJECT_PATH)
assertThat(openRequest.thread.id).isEqualTo("thread-existing")
assertThat(openRequest.subAgent).isNull()
@@ -1343,13 +1343,13 @@ class AgentSessionPromptLauncherBridgeTest {
}
@Test
fun openChatThreadRechecksPromptPlanModeAgainstLatestThreadActivity() {
fun openThreadViewThreadRechecksPromptPlanModeAgainstLatestThreadActivity() {
val providerBridge = RecordingPromptLaunchProviderBridge(
provider = AgentSessionProvider.from("codex"),
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
composedMessageBuilder = { request -> request.prompt.trim() },
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(
InMemoryAgentSessionProviderRegistry(listOf(providerBridge))
) {
@@ -1378,7 +1378,7 @@ class AgentSessionPromptLauncherBridgeTest {
)
},
projectEntriesProvider = { listOf(openProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { service, launchService ->
service.refresh()
waitForCondition {
@@ -1388,7 +1388,7 @@ class AgentSessionPromptLauncherBridgeTest {
}
val promptLaunchResults = CopyOnWriteArrayList<AgentPromptLaunchResult>()
launchService.openChatThread(
launchService.openThreadViewThread(
path = PROJECT_PATH,
thread = thread(
id = "thread-existing",
@@ -1410,7 +1410,7 @@ class AgentSessionPromptLauncherBridgeTest {
assertThat(promptLaunchResults).containsExactly(
AgentPromptLaunchResult.failure(AgentPromptLaunchError.TARGET_THREAD_BUSY_FOR_PLAN_MODE)
)
assertThat(chatOpenExecutor.openChatCalls.get()).isZero()
assertThat(threadViewOpenExecutor.openThreadViewCalls.get()).isZero()
}
}
}
@@ -1424,8 +1424,8 @@ class AgentSessionPromptLauncherBridgeTest {
)
val firstOpenStarted = CompletableDeferred<Unit>()
val releaseFirstOpen = CompletableDeferred<Unit>()
val chatOpenExecutor = RecordingChatOpenExecutor(
onOpenChat = { _, callIndex ->
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor(
onOpenThreadView = { _, callIndex ->
if (callIndex == 1) {
firstOpenStarted.complete(Unit)
releaseFirstOpen.await()
@@ -1454,7 +1454,7 @@ class AgentSessionPromptLauncherBridgeTest {
)
},
projectEntriesProvider = { listOf(openProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { service, launchService ->
service.refresh()
waitForCondition {
@@ -1471,7 +1471,7 @@ class AgentSessionPromptLauncherBridgeTest {
?.firstOrNull { thread -> thread.id == "thread-existing" }
)
launchService.openChatThread(
launchService.openThreadViewThread(
path = PROJECT_PATH,
thread = existingThread,
entryPoint = AgentWorkbenchEntryPoint.TREE_ROW
@@ -1479,7 +1479,7 @@ class AgentSessionPromptLauncherBridgeTest {
waitForCondition {
firstOpenStarted.isCompleted
}
assertThat(chatOpenExecutor.openChatCalls.get()).isEqualTo(1)
assertThat(threadViewOpenExecutor.openThreadViewCalls.get()).isEqualTo(1)
val bridge = promptLauncherBridge(service, launchService)
val request = promptLaunchRequest(targetThreadId = "thread-existing")
@@ -1493,7 +1493,7 @@ class AgentSessionPromptLauncherBridgeTest {
releaseFirstOpen.complete(Unit)
waitForCondition(timeoutMs = 5_000) {
chatOpenExecutor.openChatCalls.get() == 2
threadViewOpenExecutor.openThreadViewCalls.get() == 2
}
assertThat(providerBridge.composeCalls.get()).isEqualTo(1)
@@ -1502,15 +1502,15 @@ class AgentSessionPromptLauncherBridgeTest {
assertThat(providerBridge.lastStartupBaseLaunchSpec.get()).isNull()
assertThat(providerBridge.lastStartupPrompt.get()).isNull()
assertThat(chatOpenExecutor.openNewChatCalls.get()).isZero()
assertThat(chatOpenExecutor.openChatRequests).hasSize(2)
assertThat(threadViewOpenExecutor.openNewThreadViewCalls.get()).isZero()
assertThat(threadViewOpenExecutor.openThreadViewRequests).hasSize(2)
val initialOpen = chatOpenExecutor.openChatRequests[0]
val initialOpen = threadViewOpenExecutor.openThreadViewRequests[0]
assertThat(initialOpen.startupLaunchSpecOverride).isNull()
assertThat(initialOpen.initialComposedMessage).isNull()
assertThat(initialOpen.initialMessageToken).isNull()
val promptOpen = chatOpenExecutor.openChatRequests[1]
val promptOpen = threadViewOpenExecutor.openThreadViewRequests[1]
assertThat(promptOpen.normalizedPath).isEqualTo(PROJECT_PATH)
assertThat(promptOpen.thread.id).isEqualTo("thread-existing")
assertThat(promptOpen.subAgent).isNull()
@@ -1537,7 +1537,7 @@ class AgentSessionPromptLauncherBridgeTest {
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
startupPromptCommandSupported = false,
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(
InMemoryAgentSessionProviderRegistry(listOf(providerBridge))
) {
@@ -1559,7 +1559,7 @@ class AgentSessionPromptLauncherBridgeTest {
)
},
projectEntriesProvider = { listOf(openProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { service, launchService ->
service.refresh()
waitForCondition {
@@ -1578,14 +1578,14 @@ class AgentSessionPromptLauncherBridgeTest {
assertThat(providerBridge.createCalls.get()).isZero()
waitForCondition {
providerBridge.composeCalls.get() == 1 &&
chatOpenExecutor.openChatCalls.get() == 1
threadViewOpenExecutor.openThreadViewCalls.get() == 1
}
assertThat(providerBridge.composeCalls.get()).isEqualTo(1)
assertThat(providerBridge.lastComposeRequest.get()).isEqualTo(request.initialMessageRequest)
assertThat(providerBridge.startupCommandCalls.get()).isZero()
assertThat(providerBridge.lastStartupBaseLaunchSpec.get()).isNull()
assertThat(providerBridge.lastStartupPrompt.get()).isNull()
val openRequest = checkNotNull(chatOpenExecutor.lastOpenChatRequest.get())
val openRequest = checkNotNull(threadViewOpenExecutor.lastOpenThreadViewRequest.get())
assertThat(openRequest.normalizedPath).isEqualTo(PROJECT_PATH)
assertThat(openRequest.thread.id).isEqualTo("thread-existing")
assertThat(openRequest.subAgent).isNull()
@@ -1595,7 +1595,7 @@ class AgentSessionPromptLauncherBridgeTest {
AgentInitialMessageDispatchStep(text = "composed:Refactor selected code"),
)
assertThat(openRequest.initialMessageToken).isNotNull()
assertThat(chatOpenExecutor.openNewChatCalls.get()).isZero()
assertThat(threadViewOpenExecutor.openNewThreadViewCalls.get()).isZero()
}
}
}
@@ -1607,7 +1607,7 @@ class AgentSessionPromptLauncherBridgeTest {
provider = AgentSessionProvider.from("codex"),
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(
InMemoryAgentSessionProviderRegistry(listOf(providerBridge))
) {
@@ -1629,7 +1629,7 @@ class AgentSessionPromptLauncherBridgeTest {
)
},
projectEntriesProvider = { listOf(openProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { service, launchService ->
service.refresh()
waitForCondition {
@@ -1644,8 +1644,8 @@ class AgentSessionPromptLauncherBridgeTest {
assertThat(result.error).isEqualTo(AgentPromptLaunchError.TARGET_THREAD_NOT_FOUND)
assertThat(providerBridge.createCalls.get()).isZero()
assertThat(providerBridge.composeCalls.get()).isZero()
assertThat(chatOpenExecutor.openChatCalls.get()).isZero()
assertThat(chatOpenExecutor.openNewChatCalls.get()).isZero()
assertThat(threadViewOpenExecutor.openThreadViewCalls.get()).isZero()
assertThat(threadViewOpenExecutor.openNewThreadViewCalls.get()).isZero()
}
}
}
@@ -1653,7 +1653,7 @@ class AgentSessionPromptLauncherBridgeTest {
@Test
fun launchReturnsProviderUnavailableWhenBridgeIsMissing() {
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(
InMemoryAgentSessionProviderRegistry(emptyList())
) {
@@ -1661,7 +1661,7 @@ class AgentSessionPromptLauncherBridgeTest {
withServiceAndLaunch(
sessionSourcesProvider = { emptyList() },
projectEntriesProvider = { listOf(openProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { service, launchService ->
val bridge = promptLauncherBridge(service, launchService)
val telemetryEvents = CopyOnWriteArrayList<AgentWorkbenchTelemetryEvent>()
@@ -1672,8 +1672,8 @@ class AgentSessionPromptLauncherBridgeTest {
assertThat(result.launched).isFalse()
assertThat(result.error).isEqualTo(AgentPromptLaunchError.PROVIDER_UNAVAILABLE)
assertThat(chatOpenExecutor.openChatCalls.get()).isZero()
assertThat(chatOpenExecutor.openNewChatCalls.get()).isZero()
assertThat(threadViewOpenExecutor.openThreadViewCalls.get()).isZero()
assertThat(threadViewOpenExecutor.openNewThreadViewCalls.get()).isZero()
assertThat(telemetryEvents).contains(
AgentWorkbenchTelemetryEvent(
id = AgentWorkbenchTelemetry.PROMPT_LAUNCH_RESOLVED_EVENT_ID,
@@ -1698,7 +1698,7 @@ class AgentSessionPromptLauncherBridgeTest {
provider = AgentSessionProvider.from("codex"),
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
)
val chatOpenExecutor = RecordingChatOpenExecutor()
val threadViewOpenExecutor = RecordingThreadViewOpenExecutor()
AgentSessionProviders.withRegistryForTest(
InMemoryAgentSessionProviderRegistry(listOf(providerBridge))
) {
@@ -1706,7 +1706,7 @@ class AgentSessionPromptLauncherBridgeTest {
withServiceAndLaunch(
sessionSourcesProvider = { listOf(providerBridge.sessionSource) },
projectEntriesProvider = { listOf(openProjectEntry(PROJECT_PATH, "Project A")) },
chatOpenExecutor = chatOpenExecutor,
threadViewOpenExecutor = threadViewOpenExecutor,
) { service, launchService ->
val bridge = promptLauncherBridge(service, launchService)
val telemetryEvents = CopyOnWriteArrayList<AgentWorkbenchTelemetryEvent>()
@@ -1723,8 +1723,8 @@ class AgentSessionPromptLauncherBridgeTest {
assertThat(result.launched).isFalse()
assertThat(result.error).isEqualTo(AgentPromptLaunchError.UNSUPPORTED_LAUNCH_MODE)
assertThat(providerBridge.createCalls.get()).isZero()
assertThat(chatOpenExecutor.openChatCalls.get()).isZero()
assertThat(chatOpenExecutor.openNewChatCalls.get()).isZero()
assertThat(threadViewOpenExecutor.openThreadViewCalls.get()).isZero()
assertThat(threadViewOpenExecutor.openNewThreadViewCalls.get()).isZero()
assertThat(telemetryEvents).contains(
AgentWorkbenchTelemetryEvent(
id = AgentWorkbenchTelemetry.PROMPT_LAUNCH_RESOLVED_EVENT_ID,
@@ -2162,7 +2162,7 @@ class AgentSessionPromptLauncherBridgeTest {
}
@Test
fun addContextToOpenChatTargetDelegatesToInjectedHandler() {
fun addContextToOpenThreadViewTargetDelegatesToInjectedHandler() {
val capturedRequest = AtomicReference<AgentPromptAddContextToTargetRequest>()
val bridge = AgentSessionPromptLauncherBridge(
launchPromptRequest = { error("not used") },
@@ -2170,39 +2170,39 @@ class AgentSessionPromptLauncherBridgeTest {
pathStateResolver = ::resolveAgentSessionPathState,
refreshCatalogAndLoadNewlyOpened = {},
refreshProviderForPath = { _, _ -> },
addContextToOpenChatTarget = { request ->
addContextToOpenThreadViewTarget = { request ->
capturedRequest.set(request)
AgentPromptAddContextToTargetResult.ADDED_TO_CHAT
AgentPromptAddContextToTargetResult.ADDED_TO_THREAD_VIEW
},
)
val request = addContextToTargetRequest()
val result = runBlocking(Dispatchers.Default) {
bridge.addContextToOpenChatTarget(request)
bridge.addContextToOpenThreadViewTarget(request)
}
assertThat(result).isEqualTo(AgentPromptAddContextToTargetResult.ADDED_TO_CHAT)
assertThat(result).isEqualTo(AgentPromptAddContextToTargetResult.ADDED_TO_THREAD_VIEW)
assertThat(capturedRequest.get()).isEqualTo(request)
}
@Test
fun addContextToOpenChatTargetPropagatesAlreadyAddedResult() {
fun addContextToOpenThreadViewTargetPropagatesAlreadyAddedResult() {
val bridge = AgentSessionPromptLauncherBridge(
launchPromptRequest = { error("not used") },
stateFlowProvider = { error("not used") },
pathStateResolver = ::resolveAgentSessionPathState,
refreshCatalogAndLoadNewlyOpened = {},
refreshProviderForPath = { _, _ -> },
addContextToOpenChatTarget = {
AgentPromptAddContextToTargetResult.ALREADY_ADDED_TO_CHAT
addContextToOpenThreadViewTarget = {
AgentPromptAddContextToTargetResult.ALREADY_ADDED_TO_THREAD_VIEW
},
)
val result = runBlocking(Dispatchers.Default) {
bridge.addContextToOpenChatTarget(addContextToTargetRequest())
bridge.addContextToOpenThreadViewTarget(addContextToTargetRequest())
}
assertThat(result).isEqualTo(AgentPromptAddContextToTargetResult.ALREADY_ADDED_TO_CHAT)
assertThat(result).isEqualTo(AgentPromptAddContextToTargetResult.ALREADY_ADDED_TO_THREAD_VIEW)
}
}
@@ -2226,13 +2226,13 @@ private fun addContextToTargetRequest(): AgentPromptAddContextToTargetRequest {
}
private fun <T> withOpenInNonDedicatedFrameSettingForTest(action: () -> T): T {
val previousValue = AgentChatOpenModeSettings.openInDedicatedFrame()
AgentChatOpenModeSettings.setOpenInDedicatedFrame(false)
val previousValue = AgentThreadViewOpenModeSettings.openInDedicatedFrame()
AgentThreadViewOpenModeSettings.setOpenInDedicatedFrame(false)
try {
return action()
}
finally {
AgentChatOpenModeSettings.setOpenInDedicatedFrame(previousValue)
AgentThreadViewOpenModeSettings.setOpenInDedicatedFrame(previousValue)
}
}
@@ -16,6 +16,7 @@ targets:
- ../../sessions-actions/src/actions/NewThreadMenuActions.kt
- ../../sessions/src/service/AgentSessionPromptLauncherBridge.kt
- ../../sessions/src/service/AgentSessionLaunchService.kt
- ../../sessions/src/state/AgentSessionUiPreferencesStateService.kt
- ../../prompt/ui/testSrc/*.kt
- ../../sessions/testSrc/AgentSessionPromptLauncherBridgeTest.kt
---
@@ -23,7 +24,7 @@ targets:
# Global Prompt Entry
Status: Draft
Date: 2026-05-16
Date: 2026-06-30
## Summary
The global prompt opens a project-scoped prompt surface for starting a new task or sending a prompt to an existing loaded task. The surface is normally a popup, but the empty editor state may host a compact inline prompt composer instead of standard empty-state hints. This spec owns popup and inline lifecycle, target mode, validation, keyboard behavior, provider selection, and launcher handoff. Context collection and Add-to-Agent-Context routing are specified separately.
@@ -34,7 +35,7 @@ The global prompt opens a project-scoped prompt surface for starting a new task
- When the main editor area is empty, Agent Workbench contributes a compact inline composer to the platform empty editor state. The composer owns the rich empty-state surface, uses the shared prompt content/session machinery, and suppresses the painted empty editor action hints while it is visible. Standard empty-state hints remain a fallback when no rich composer is available.
[@test] ../../prompt/ui/testSrc/emptyState/AgentWorkbenchInlinePromptEmptyStateProviderTest.kt
[@test] ../../../platform/platform-impl/testSrc/com/intellij/openapi/fileEditor/impl/EditorEmptyTextPainterTest.kt
[@test] ../../../../platform/platform-impl/testSrc/com/intellij/openapi/fileEditor/impl/EditorEmptyTextPainterTest.kt
- The inline empty-state composer is gated by the `agent.workbench.inline.empty.state.prompt` system property, enabled by default. When enabled, the inline provider creates the composer and the redundant `AgentWorkbenchPrompt.OpenGlobalPalette` promoted-text hint is suppressed. When disabled, the inline provider creates no component and the promoted-text hint is shown as the empty-editor affordance instead.
[@test] ../../prompt/ui/testSrc/emptyState/AgentWorkbenchInlinePromptEmptyStateProviderTest.kt
@@ -55,13 +56,13 @@ The global prompt opens a project-scoped prompt surface for starting a new task
[@test] ../../prompt/ui/testSrc/AgentPromptPaletteViewStructureTest.kt
[@test] ../../prompt/ui/testSrc/AgentPromptPaletteViewLayoutTest.kt
- Existing-task mode observes loaded threads through `AgentPromptLauncherBridge`, scoped to the resolved working project path, and may preselect the focused open chat thread or a single loaded thread without automatically switching modes.
- Existing-task mode observes loaded threads through `AgentPromptLauncherBridge`, scoped to the resolved working project path, and may preselect the focused open thread view thread or a single loaded thread without automatically switching modes.
[@test] ../../prompt/ui/testSrc/AgentPromptExistingTaskControllerTest.kt
- Submit validation must block empty prompts, missing provider, unavailable provider CLI, missing project path, missing launcher bridge, and existing-task submits without a selected task.
[@test] ../../prompt/ui/testSrc/AgentPromptSubmitValidationDecisionsTest.kt
- Working project path resolution must never use the dedicated-frame project path. In a dedicated Agent frame, it resolves from selected Sessions context, selected chat tab source path, then most recent non-dedicated project; unresolved submits prompt for a source project and keep the popup open on cancel.
- Working project path resolution must never use the dedicated-frame project path. In non-dedicated frames it resolves from the current open project's identity path, so Bazel projects contribute their `.bazelproject` identity instead of raw `project.basePath`. In a dedicated Agent frame, it resolves from selected Sessions context, selected Thread View source path, then most recent non-dedicated project; unresolved submits prompt for a source project and keep the popup open on cancel.
[@test] ../../sessions/testSrc/AgentSessionPromptLauncherBridgeTest.kt
- Keyboard behavior is: Enter submits, Shift+Enter inserts a line break, Tab/Shift+Tab switch prompt tabs unless completion or Codex tab-queue handling consumes the key.
@@ -69,7 +70,7 @@ The global prompt opens a project-scoped prompt surface for starting a new task
[@test] ../../prompt/ui/testSrc/AgentPromptFooterHintDecisionsTest.kt
- Claude slash completion is available only for Claude provider prompts and only for explicit slash-token completion; it merges built-in Claude menu commands with project `.claude/commands` and `.claude/skills` definitions.
[@test] ../../prompt/ui/testSrc/AgentPromptClaudeSlashCompletionProviderTest.kt
[@test] ../../prompt/ui/testSrc/AgentPromptCommandCompletionProviderTest.kt
- New-task provider, mode, and generation defaults restore through launch profiles. Prompt drafts may persist provider options and container mode, but they must not persist a separate provider id default.
[@test] ../../prompt/ui/testSrc/AgentPromptLaunchProfileStateTest.kt
@@ -86,20 +87,26 @@ The global prompt opens a project-scoped prompt surface for starting a new task
- Inline empty-state mode is always a `NEW_TASK` prompt. It must not restore `EXISTING_TASK` draft mode or extension-tab auto-selection because those controls are hidden in the compact empty-state surface.
[@test] ../../prompt/ui/testSrc/emptyState/AgentWorkbenchInlinePromptEmptyStateProviderTest.kt
- Inline new-thread mode is also always a `NEW_TASK` prompt. It is hosted inside a deferred chat tab, starts from the selected launch profile, skips extension-tab auto-selection, and keeps the inline prompt visible when `AgentPromptLauncherBridge.launch(...)` returns a failure so the same pending tab can be retried.
- Inline new-thread mode is also always a `NEW_TASK` prompt. It is hosted inside a deferred Thread View, starts from the selected launch profile, skips extension-tab auto-selection, and keeps the inline prompt visible when `AgentPromptLauncherBridge.launch(...)` returns a failure so the same pending tab can be retried.
[@test] ../../prompt/ui/testSrc/AgentPromptPaletteSessionControllerTest.kt
[@test] ../../prompt/ui/testSrc/AgentPromptPaletteSubmitControllerTest.kt
[@test] ../../sessions/testSrc/AgentSessionLaunchServiceTest.kt
- Plan mode is available only when the selected provider exposes the plan-mode option, persists in project prompt draft state, and is forced off/rejected for busy existing tasks. A typed `/plan` prefix remains prompt text and does not toggle the option.
- New-task launches accepted from the global prompt use the shared generic new-thread deferred tab: provider-neutral centered copy appears immediately, and the spinner appears only after a short delay.
[@test] ../../thread-view/testSrc/AgentThreadViewFileEditorLifecycleTest.kt
[@test] ../../sessions/testSrc/AgentSessionLaunchServiceTest.kt
- Plan mode is available as a compact header icon toggle only when the selected provider exposes the plan-mode option. Its last-used state is stored per provider as provider option preferences, restored on prompt open, sent in the launch payload, and forced off or rejected for busy existing tasks. The prompt UI filters selectable options before submit, but launch service preserves explicit launch-request option ids so providers can resolve capability-dependent behavior. A typed `/plan` prefix remains prompt text and does not toggle the option.
[@test] ../../prompt/ui/testSrc/AgentPromptPlanModeDecisionsTest.kt
[@test] ../../sessions/testSrc/AgentSessionPromptLauncherBridgeTest.kt
[@test] ../../lib-agent/providers/junie/sessions/testSrc/JunieExistingThreadPromptLaunchIntegrationTest.kt
[@test] ../../lib-agent/providers/junie/sessions/testSrc/JunieNewThreadPromptLaunchIntegrationTest.kt
- Codex `NEW_TASK` prompts start through the Codex app-server remote-resume path. Standard prompts start plain app-server turns; Plan prompts do not type `/plan`, and the resumed TUI must visibly enter Plan mode before the prompt starts. An acknowledged no-op `thread/settings/update` without `thread/settings/updated` must not be treated as prompt-send failure.
[@test] ../../lib-agent/providers/codex/sessions/testSrc/CodexPlanPromptRealAppServerIntegrationTest.kt
[@test] ../../lib-agent/providers/codex/sessions/testSrc/CodexNewThreadPromptLaunchIntegrationTest.kt
- `NEW_TASK` and `EXISTING_TASK` expose the provider selector. Changing provider in `EXISTING_TASK` reloads the selectable task list for that provider. Provider-backed model and reasoning-effort controls are exposed for `NEW_TASK` as specified by `global-prompt-generation-controls.spec.md`. Header actions stay limited to prompt-surface tools such as provider selection, Plan mode, Run in container, preview, and prompt library.
- `NEW_TASK` and `EXISTING_TASK` expose the provider selector. Changing provider in `EXISTING_TASK` reloads the selectable task list for that provider. Provider-backed model and reasoning-effort controls are exposed for `NEW_TASK` through the unified launch-settings control specified by `global-prompt-generation-controls.spec.md`. The prompt composer mental model, context/text/tray ownership, and content-lane layout contract are owned by `global-prompt-composer.spec.md`.
[@test] ../../prompt/ui/testSrc/AgentPromptPaletteViewStructureTest.kt
[@test] ../../prompt/ui/testSrc/AgentPromptProviderSelectorTest.kt
[@test] ../../prompt/ui/testSrc/AgentPromptPaletteSessionControllerTest.kt
@@ -123,10 +130,11 @@ The global prompt opens a project-scoped prompt surface for starting a new task
- The empty editor inline composer is a compact persistent empty-state affordance, not a full embedded popup.
- Empty editor rendering must not steal focus; users focus the inline composer by clicking it or invoking the global prompt action while it is visible.
- The inline prompt editor exposes localized accessible name and description metadata, while validation and status feedback continue to use the shared prompt status strip behavior.
- The inline prompt starts compact, grows with longer prompt text up to a bounded height, then scrolls inside the editor.
- The popup keep-open toggle is a secondary footer control, not part of the primary header action cluster.
- Validation errors appear inline and keep the popup open.
- Successful launches close the popup and clear the submitted draft.
- Successful inline new-thread launches clear the submitted draft and replace the prompt surface by starting the deferred chat tab.
- Successful inline new-thread launches clear the submitted draft and replace the prompt surface by starting the deferred Thread View.
## Testing / Local Run
- `./tests.cmd --module intellij.agent.workbench.prompt.ui.tests --test "com.intellij.agent.workbench.prompt.ui.AgentPrompt*Test"`
@@ -137,6 +145,7 @@ The global prompt opens a project-scoped prompt surface for starting a new task
- `./tests.cmd --module intellij.agent.workbench.sessions.tests --test com.intellij.agent.workbench.sessions.AgentSessionLaunchServiceTest`
## References
- `global-prompt-composer.spec.md`
- `add-to-agent-context.spec.md`
- `global-prompt-generation-controls.spec.md`
- `global-prompt-suggestions.spec.md`
@@ -5,8 +5,8 @@ targets:
- ../../sessions-toolwindow/src/**/*.kt
- ../../sessions-actions/src/**/*.kt
- ../../sessions/src/service/AgentSessionLaunchService.kt
- ../../chat/src/AgentChatDeferredStartContent.kt
- ../../chat/src/AgentChatFileEditor.kt
- ../../thread-view/src/AgentThreadViewDeferredStartContent.kt
- ../../thread-view/src/AgentThreadViewFileEditor.kt
- ../../sessions/src/service/AgentSessionProjectCatalog.kt
- ../../sessions/resources/messages/AgentSessionsBundle.properties
- ../../sessions-actions/resources/intellij.agent.workbench.sessions.actions.xml
@@ -18,7 +18,7 @@ targets:
# Agent Sessions New-Thread Actions
Status: Draft
Date: 2026-05-09
Date: 2026-06-28
## Summary
New-thread actions let users start provider-backed threads from project/worktree rows and the main toolbar. This spec owns action availability, launch-profile menus, target resolution, inline prompt handoff, and launch deduplication. Codex pending/concrete rebind behavior is specified separately.
@@ -51,9 +51,13 @@ New-thread actions let users start provider-backed threads from project/worktree
toolbar action is the supported new-thread entry point for the single-project UI.
[@test] ../../sessions-toolwindow/testSrc/AgentSessionsTreePopupActionsTest.kt
- Dedicated Agent frame main-toolbar new-thread resolves source projects lazily on click or popup expansion. Multiple source candidates require explicit selection; a single candidate may be used directly; selected chat-tab source path is a fallback when no open source-project candidate exists.
- Dedicated Agent frame main-toolbar new-thread resolves source projects lazily on click or popup expansion. Multiple source candidates require explicit selection; a single candidate may be used directly; selected thread view-tab source path is a fallback when no open source-project candidate exists.
[@test] ../../sessions-actions/testSrc/AgentSessionsMainToolbarNewThreadActionsTest.kt
- New-thread target resolution uses the source project identity path for row selection, thread view-tab keys, and session state, while launch requests carry the resolved project directory for provider cwd. For Bazel project-view opens, the identity is the `.bazelproject` path and the project directory is the containing workspace/Git root.
[@test] ../../sessions/testSrc/AgentSessionPromptLauncherBridgeTest.kt
[@test] ../../sessions/testSrc/AgentSessionLaunchServiceTest.kt
- Source-project labels in dedicated-frame new-thread popups reuse Sessions tree naming and fall back to full normalized paths for collisions.
[@test] ../../sessions/testSrc/AgentSessionProjectCatalogTest.kt
[@test] ../../sessions-actions/testSrc/AgentSessionsMainToolbarNewThreadActionsTest.kt
@@ -61,7 +65,7 @@ New-thread actions let users start provider-backed threads from project/worktree
- Main-toolbar new-thread is one split-button action on `MainToolbarRight`, after `NewUiRunWidget`. Icon click quick-launches only for a direct eligible target; otherwise it opens the same launch-profile picker as the chevron.
[@test] ../../sessions-actions/testSrc/AgentSessionsMainToolbarNewThreadActionsTest.kt
- Main-toolbar target resolution prefers chat context path, selected chat source project path, then `project.basePath`; in dedicated Agent frames it uses the lazy source-candidate path.
- Main-toolbar target resolution prefers thread view context path, selected thread view source project path, then `project.basePath`; in dedicated Agent frames it uses the lazy source-candidate path.
[@test] ../../sessions-actions/testSrc/AgentSessionsMainToolbarNewThreadActionsTest.kt
- Launching must go through `AgentSessionLaunchService.createNewSession(...)`, update persisted provider-option state on accepted prompt-capable launches, and deduplicate semantically identical in-flight launches with single-flight drop semantics.
@@ -69,25 +73,32 @@ New-thread actions let users start provider-backed threads from project/worktree
[@test] ../../sessions/testSrc/AgentSessionLaunchServiceTest.kt
[@test] ../../sessions/testSrc/AgentSessionRefreshCoordinatorTest.kt
- Main-toolbar new-thread opens a deferred chat tab with an inline prompt when `agent.workbench.new.thread.inline.prompt` is enabled and the selected provider supports prompt launch. The inline prompt is seeded with the selected launch profile, submits through `AgentDeferredNewSessionHandle.launch(...)`, and completes the same pending chat tab instead of opening a separate TUI tab first.
- Main-toolbar new-thread opens a deferred Thread View with an inline prompt when `agent.workbench.new.thread.inline.prompt` is enabled and the selected provider supports prompt launch. The inline prompt is seeded with the selected launch profile, submits through `AgentDeferredNewSessionHandle.launch(...)`, and completes the same pending Thread View instead of opening a separate TUI tab first.
[@test] ../../sessions-actions/testSrc/AgentSessionsMainToolbarNewThreadActionsTest.kt
[@test] ../../sessions/testSrc/AgentSessionLaunchServiceTest.kt
[@test] ../../prompt/ui/testSrc/AgentPromptPaletteSessionControllerTest.kt
[@test] ../../chat/testSrc/AgentChatFileEditorLifecycleTest.kt
[@test] ../../thread-view/testSrc/AgentThreadViewFileEditorLifecycleTest.kt
- Inline new-thread routing falls back to direct `createNewSession(...)` when the registry key is disabled, the provider descriptor is missing, the provider does not support prompt launch, deferred chat opening fails, or inline prompt installation fails.
- Main-toolbar new-thread captures the toolbar `AnActionEvent` data context at the UI action boundary. Inline prompt launches reuse that invocation data, and non-inline launches collect the same default prompt context into the initial message request, auto-trimming oversized context without showing an extra dialog.
[@test] ../../sessions-actions/testSrc/AgentSessionsMainToolbarNewThreadActionsTest.kt
- Generic new-thread deferred tabs show provider-neutral centered regular-weight progress copy with secondary detail text when present. The spinner appears only after a short delay so quick launches do not flash progress chrome.
[@test] ../../sessions/testSrc/AgentSessionLaunchServiceTest.kt
[@test] ../../thread-view/testSrc/AgentThreadViewFileEditorLifecycleTest.kt
- Inline new-thread routing falls back to direct `createNewSession(...)` when the registry key is disabled, the provider descriptor is missing, the provider does not support prompt launch, deferred thread view opening fails, or inline prompt installation fails.
[@test] ../../sessions-actions/testSrc/AgentSessionsMainToolbarNewThreadActionsTest.kt
[@test] ../../sessions/testSrc/AgentSessionLaunchServiceTest.kt
- Command construction for each provider and launch mode follows `../core/agent-core-contracts.spec.md`.
[@test] ../../claude/sessions/testSrc/ClaudeAgentSessionProviderDescriptorTest.kt
[@test] ../../codex/sessions/testSrc/CodexAgentSessionProviderDescriptorTest.kt
[@test] ../../junie/sessions/testSrc/JunieAgentSessionProviderDescriptorTest.kt
[@test] ../../lib-agent/providers/claude/sessions/testSrc/ClaudeAgentSessionProviderDescriptorTest.kt
[@test] ../../lib-agent/providers/codex/sessions/testSrc/CodexAgentSessionProviderDescriptorTest.kt
[@test] ../../lib-agent/providers/junie/sessions/testSrc/JunieAgentSessionProviderDescriptorTest.kt
## User Experience
- Quick actions use the active launch profile when that profile is still launchable, otherwise they fall back to the first launchable built-in/user profile.
- Launch-profile pickers keep Standard and YOLO choices explicit.
- Registry-gated inline new-thread uses the chat tab itself as the prompt surface; failed submit keeps that prompt visible for correction or retry.
- Registry-gated inline new-thread uses the Thread View itself as the prompt surface; failed submit keeps that prompt visible for correction or retry.
- Dedicated-frame source selection appears only when the user invokes the action, not during toolbar update.
## Testing / Local Run
@@ -95,7 +106,7 @@ New-thread actions let users start provider-backed threads from project/worktree
- `./tests.cmd --module intellij.agent.workbench.sessions.toolwindow.tests --test com.intellij.agent.workbench.sessions.toolwindow.AgentSessionsTreePopupActionsTest`
- `./tests.cmd --module intellij.agent.workbench.sessions.actions.tests --test com.intellij.agent.workbench.sessions.AgentSessionsGearActionsTest`
- `./tests.cmd --module intellij.agent.workbench.sessions.actions.tests --test com.intellij.agent.workbench.sessions.AgentSessionsMainToolbarNewThreadActionsTest`
- `./tests.cmd --module intellij.agent.workbench.chat.tests --test com.intellij.agent.workbench.chat.AgentChatFileEditorLifecycleTest`
- `./tests.cmd --module intellij.agent.workbench.thread.view.tests --test com.intellij.agent.workbench.thread.view.AgentThreadViewFileEditorLifecycleTest`
- `./tests.cmd --module intellij.agent.workbench.prompt.ui.tests --test com.intellij.agent.workbench.prompt.ui.AgentPromptPaletteSessionControllerTest`
- `./tests.cmd --module intellij.agent.workbench.sessions.tests --test com.intellij.agent.workbench.sessions.AgentSessionPromptLauncherBridgeTest`
- `./tests.cmd --module intellij.agent.workbench.sessions.tests --test com.intellij.agent.workbench.sessions.AgentSessionLaunchServiceTest`
@@ -1,60 +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.chat
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.annotations.ApiStatus
import javax.swing.JComponent
@ApiStatus.Internal
class AgentChatDeferredStartContent(
@JvmField val component: JComponent,
@JvmField val preferredFocusedComponent: JComponent? = null,
private val disposeContent: () -> Unit = {},
) {
private var disposed: Boolean = false
fun dispose() {
if (disposed) {
return
}
disposed = true
disposeContent()
}
}
@ApiStatus.Internal
fun installAgentChatDeferredStartContent(
project: Project,
file: VirtualFile,
content: AgentChatDeferredStartContent,
): Boolean {
val chatFile = file as? AgentChatVirtualFile ?: return false
chatFile.replaceDeferredStartContent(content)
FileEditorManager.getInstance(project).getEditors(chatFile).forEach { editor ->
(editor as? AgentChatFileEditor)?.refreshForFileStateChange()
}
return true
}
private val AGENT_CHAT_DEFERRED_START_CONTENT_KEY: Key<AgentChatDeferredStartContent> =
Key.create("agent.workbench.chat.deferred.start.content")
internal fun AgentChatVirtualFile.deferredStartContent(): AgentChatDeferredStartContent? {
return getUserData(AGENT_CHAT_DEFERRED_START_CONTENT_KEY)
}
internal fun AgentChatVirtualFile.replaceDeferredStartContent(content: AgentChatDeferredStartContent?) {
val previous = deferredStartContent()
if (previous === content) {
return
}
putUserData(AGENT_CHAT_DEFERRED_START_CONTENT_KEY, content)
previous?.dispose()
}
internal fun AgentChatVirtualFile.clearDeferredStartContent() {
replaceDeferredStartContent(null)
}
@@ -1,7 +1,10 @@
// 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.thread.view
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.annotations.ApiStatus
import javax.swing.JComponent
@@ -22,8 +25,27 @@ class AgentThreadViewDeferredStartContent(
}
}
<<<<<<<< HEAD:community/plugins/agent-workbench/thread-view/src/AgentChatDeferredStartContent.kt
@ApiStatus.Internal
fun installAgentChatDeferredStartContent(
project: Project,
file: VirtualFile,
content: AgentChatDeferredStartContent,
): Boolean {
val chatFile = file as? AgentChatVirtualFile ?: return false
chatFile.replaceDeferredStartContent(content)
FileEditorManager.getInstance(project).getEditors(chatFile).forEach { editor ->
(editor as? AgentChatFileEditor)?.refreshForFileStateChange()
}
return true
}
private val AGENT_CHAT_DEFERRED_START_CONTENT_KEY: Key<AgentChatDeferredStartContent> =
Key.create("agent.workbench.chat.deferred.start.content")
========
private val AGENT_THREAD_VIEW_DEFERRED_START_CONTENT_KEY: Key<AgentThreadViewDeferredStartContent> =
Key.create("agent.workbench.threadView.deferred.start.content")
>>>>>>>> 968b450c78c65 (IJPL-248623 rename Agent Workbench chat UI to thread view):community/plugins/agent-workbench/thread-view/src/AgentThreadViewDeferredStartContent.kt
internal fun AgentThreadViewVirtualFile.deferredStartContent(): AgentThreadViewDeferredStartContent? {
return getUserData(AGENT_THREAD_VIEW_DEFERRED_START_CONTENT_KEY)
@@ -1,7 +1,7 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.agent.workbench.chat
package com.intellij.agent.workbench.thread.view
// @spec community/plugins/agent-workbench/spec/chat/agent-chat-editor.spec.md
// @spec community/plugins/agent-workbench/spec/thread-view/agent-thread-view.spec.md
import com.intellij.CommonBundle
import com.intellij.agent.workbench.ui.AgentWorkbenchActionIds
@@ -14,7 +14,10 @@ import com.intellij.platform.ai.agent.sessions.core.AgentSessionThreadRebindPoli
import com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchIntent
import com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchOperation
import com.intellij.platform.ai.agent.sessions.core.launch.AgentSessionLaunchPlanner
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.providers.AgentInitialPromptDeliveryChannel
import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionArchivedSource
import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionLaunchProfileResolver
import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProviderDescriptor
import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProviders
@@ -25,6 +28,7 @@ import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.EDT
import com.intellij.openapi.components.serviceAsync
import com.intellij.openapi.components.serviceOrNull
@@ -39,6 +43,9 @@ import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.UserDataHolderBase
import com.intellij.openapi.wm.StatusBar
import com.intellij.terminal.frontend.view.TerminalInputInterceptor
import com.intellij.util.ui.AsyncProcessIcon
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CoroutineScope
@@ -51,36 +58,46 @@ import kotlinx.coroutines.withContext
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.TestOnly
import java.awt.BorderLayout
import java.awt.Component
import java.awt.Container
import java.awt.Dimension
import java.awt.GridBagLayout
import java.awt.event.HierarchyEvent
import java.awt.event.HierarchyListener
import java.awt.event.KeyEvent
import java.beans.PropertyChangeListener
import javax.swing.BorderFactory
import javax.swing.Box
import javax.swing.BoxLayout
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.JProgressBar
import javax.swing.JTextArea
import javax.swing.Timer
import kotlin.coroutines.resume
internal class AgentChatFileEditor(
private const val DEFERRED_START_PROGRESS_DELAY_MS = 300
private const val DEFERRED_START_PROGRESS_NAME = "Agent Thread View Start Progress"
private const val DEFERRED_START_PROGRESS_TIMER_PROPERTY = "AgentThreadViewFileEditor.deferredStartProgressTimer"
internal class AgentThreadViewFileEditor(
private val project: Project,
private val file: AgentChatVirtualFile,
private val terminalTabs: AgentChatTerminalTabs = ToolWindowAgentChatTerminalTabs,
private val liveTerminalRegistry: AgentChatLiveTerminalRegistry? = null,
private val tabSnapshotWriter: AgentChatTabSnapshotWriter = ApplicationAgentChatTabSnapshotWriter,
private val archivedRestoreHandler: AgentChatArchivedRestoreHandler = ApplicationAgentChatArchivedRestoreHandler,
private val file: AgentThreadViewVirtualFile,
private val terminalTabs: AgentThreadViewTerminalTabs = ToolWindowAgentThreadViewTerminalTabs,
private val liveTerminalRegistry: AgentThreadViewLiveTerminalRegistry? = null,
private val tabSnapshotWriter: AgentThreadViewTabSnapshotWriter = ApplicationAgentThreadViewTabSnapshotWriter,
private val archivedRestoreHandler: AgentThreadViewArchivedRestoreHandler = ApplicationAgentThreadViewArchivedRestoreHandler,
private val currentTimeProvider: () -> Long = System::currentTimeMillis,
private val pendingScopedRefreshRetryIntervalMs: Long = AgentSessionThreadRebindPolicy.PENDING_THREAD_REFRESH_RETRY_INTERVAL_MS,
editorCoroutineScope: CoroutineScope? = null,
private val providerDescriptorResolver: (AgentSessionProvider) -> AgentSessionProviderDescriptor? = AgentSessionProviders::find,
private val behaviorResolver: (AgentSessionProvider?) -> AgentChatProviderBehavior = ::resolveAgentChatProviderBehavior,
private val customContentProviderResolver: (AgentThreadViewContentContext) -> AgentThreadViewCustomContentProvider? = AgentThreadViewCustomContent::find,
private val behaviorResolver: (AgentSessionProvider?) -> AgentThreadViewProviderBehavior = ::resolveAgentThreadViewProviderBehavior,
) : UserDataHolderBase(), FileEditor {
private val ownedTerminalStartupJob = if (editorCoroutineScope == null) SupervisorJob() else null
@Suppress("RAW_SCOPE_CREATION")
private val terminalStartupScope = editorCoroutineScope ?: CoroutineScope(checkNotNull(ownedTerminalStartupJob) + Dispatchers.Default)
private val component = AgentChatFileEditorComponent()
private val component = AgentThreadViewFileEditorComponent()
private fun buildEditorTabActions(): ActionGroup? {
val actionManager = ActionManager.getInstance()
@@ -94,27 +111,29 @@ internal class AgentChatFileEditor(
actionManager.getAction(actionId)?.let(::add)
}
}
return buildAgentChatEditorTabActionGroup(actions)
return buildAgentThreadViewEditorTabActionGroup(actions)
}
private var cachedTabActionsProvider: AgentSessionProvider? = null
private var cachedTabActions: ActionGroup? = null
private var cachedTabActionsInitialized: Boolean = false
private var tab: AgentChatTerminalTab? = null
private var pendingContextPanel: AgentChatPendingContextPanel? = null
private var tab: AgentThreadViewTerminalTab? = null
private var pendingContextPanel: AgentThreadViewPendingContextPanel? = null
private var pendingContextPanelInstalled: Boolean = false
private var initializationStarted: Boolean = false
private var initializationRequested: Boolean = false
private var customContentInstalled: Boolean = false
private var customContentPreferredFocusedComponent: JComponent? = null
private var focusTerminalAfterInitialization: Boolean = false
private var stateApplied: Boolean = file.projectPath.isNotBlank() || file.threadIdentity.isNotBlank()
private var initializationJob: Job? = null
private var disposed: Boolean = false
private var pendingThreadRefreshController: AgentChatPendingThreadRefreshController? = null
private var terminalTitleThreadRebindController: AgentChatDisposableController? = null
private var concreteThreadRebindController: AgentChatConcreteThreadRebindController? = null
private var initialMessageDispatcher: AgentChatInitialMessageDispatcher? = null
private var scopedTerminalRefreshController: AgentChatDisposableController? = null
private var terminalRestoreContextController: AgentChatDisposableController? = null
private var pendingThreadRefreshController: AgentThreadViewPendingThreadRefreshController? = null
private var terminalTitleThreadRebindController: AgentThreadViewDisposableController? = null
private var concreteThreadRebindController: AgentThreadViewConcreteThreadRebindController? = null
private var initialMessageDispatcher: AgentThreadViewInitialMessageDispatcher? = null
private var scopedTerminalRefreshController: AgentThreadViewDisposableController? = null
private var terminalRestoreContextController: AgentThreadViewDisposableController? = null
private var crossProjectDockTargetRegistration: Disposable? = null
private val providerDescriptor
@@ -124,10 +143,11 @@ internal class AgentChatFileEditor(
override fun getPreferredFocusedComponent(): JComponent {
file.deferredStartContent()?.preferredFocusedComponent?.let { return it }
customContentPreferredFocusedComponent?.let { return it }
return tab?.preferredFocusableComponent ?: component
}
override fun getName(): String = AgentChatBundle.message("chat.filetype.name")
override fun getName(): String = AgentThreadViewBundle.message("thread.view.filetype.name")
override fun getTabActions(): ActionGroup? {
val provider = file.provider
@@ -141,20 +161,20 @@ internal class AgentChatFileEditor(
override fun getState(level: FileEditorStateLevel): FileEditorState {
if (!file.shouldRestoreOnRestart() || file.projectPath.isBlank() || file.threadIdentity.isBlank()) {
return AgentChatFileEditorState(snapshot = null)
return AgentThreadViewFileEditorState(snapshot = null)
}
return AgentChatFileEditorState(snapshot = file.toSnapshot(), startupIntent = file.startupIntent())
return AgentThreadViewFileEditorState(snapshot = file.toSnapshot(), startupIntent = file.startupIntent())
}
override fun setState(state: FileEditorState) {
val chatState = state as? AgentChatFileEditorState ?: return
val threadViewState = state as? AgentThreadViewFileEditorState ?: return
stateApplied = true
val providerBeforeUpdate = file.provider
val snapshot = chatState.snapshot
val snapshot = threadViewState.snapshot
if (snapshot != null) {
file.updateRestoreOnRestart(true)
file.updateFromResolution(AgentChatTabResolution.Resolved(snapshot))
file.updateStartupIntent(chatState.startupIntent)
file.updateFromResolution(AgentThreadViewTabResolution.Resolved(snapshot))
file.updateStartupIntent(threadViewState.startupIntent)
ensureCrossProjectDockTargetRegistration()
FileEditorManager.getInstance(project).updateFilePresentation(file)
}
@@ -178,7 +198,7 @@ internal class AgentChatFileEditor(
override fun removePropertyChangeListener(listener: PropertyChangeListener) = Unit
override fun getFile(): AgentChatVirtualFile = file
override fun getFile(): AgentThreadViewVirtualFile = file
override fun selectNotify() {
if (tab == null) {
@@ -204,6 +224,7 @@ internal class AgentChatFileEditor(
pendingContextPanel = null
pendingContextPanelInstalled = false
file.clearDeferredStartContent()
disposeDeferredStartProgressTimers(component)
component.removeAll()
}
@@ -220,10 +241,13 @@ internal class AgentChatFileEditor(
renderDeferredStartState(checkNotNull(deferredStartState))
return
}
if (tryInstallCustomContent()) {
return
}
if (initializationStarted) {
return
}
val validationError = validateAgentChatFile(file)
val validationError = validateAgentThreadViewFile(file)
if (validationError != null) {
handleRestoreValidationError(validationError)
return
@@ -233,7 +257,7 @@ internal class AgentChatFileEditor(
val suppressInitialMessageDispatch = startupLaunchSpecOverride != null && file.consumeSuppressInitialMessageDispatchOnStartup()
val startupIntent = file.startupIntent()
if (startupLaunchSpecOverride == null && file.isPendingThread && startupIntent == null) {
handleRestoreValidationError(AgentChatBundle.message("chat.restore.validation.pending.thread"))
handleRestoreValidationError(AgentThreadViewBundle.message("thread.view.restore.validation.pending.thread"))
return
}
initializationJob = terminalStartupScope.launch {
@@ -252,18 +276,47 @@ internal class AgentChatFileEditor(
throw e
}
catch (e: Throwable) {
AgentChatRestoreNotificationService.reportTerminalInitializationFailure(project, file, e)
AgentThreadViewRestoreNotificationService.reportTerminalInitializationFailure(project, file, e)
}
}
}
private suspend fun isRestoredArchivedThread(descriptor: AgentSessionProviderDescriptor?): Boolean {
val source = descriptor?.sessionSource ?: return false
if (!source.supportsArchivedThreads) {
return false
/**
* Installs provider-supplied non-terminal content (e.g. the ACP screen) into the same
* editor tab and skips the terminal lifecycle. Returns true when custom content owns this tab.
*/
private fun tryInstallCustomContent(): Boolean {
if (customContentInstalled) {
return true
}
val contentContext = resolveCustomContentContext() ?: return false
val contentProvider = customContentProviderResolver(contentContext) ?: return false
val deferredStartState = file.deferredStartState
if (deferredStartState?.phase == AgentThreadViewDeferredStartPhase.READY_TO_START) {
file.updateDeferredStartState(null)
}
customContentInstalled = true
initializationStarted = true
ensureCrossProjectDockTargetRegistration()
val customComponent = contentProvider.createComponent(
project = project,
context = contentContext,
parent = this,
)
customContentPreferredFocusedComponent = (customComponent as? AgentThreadViewPreferredFocusableContent)?.preferredFocusedComponent
disposeDeferredStartProgressTimers(component)
component.removeAll()
component.add(customComponent, BorderLayout.CENTER)
component.revalidate()
component.repaint()
file.updateStartupIntent(null)
return true
}
private suspend fun isRestoredArchivedThread(descriptor: AgentSessionProviderDescriptor?): Boolean {
val source = descriptor?.sessionSource as? AgentSessionArchivedSource ?: return false
val archivedThreads = try {
source.listArchivedThreadsFromOpenProject(path = file.projectPath, project = project)
source.listArchivedThreads(path = file.projectPath, openProject = project)
}
catch (e: CancellationException) {
throw e
@@ -271,7 +324,7 @@ internal class AgentChatFileEditor(
catch (_: Throwable) {
return false
}
return archivedThreads.any { thread -> thread.matchesRestoredAgentChatFile(file) }
return archivedThreads.any { thread -> thread.matchesRestoredAgentThreadViewFile(file) }
}
private suspend fun awaitEditorComponentShowing() {
@@ -285,36 +338,40 @@ internal class AgentChatFileEditor(
}
}
private suspend fun resolveLiveTerminalRegistry(): AgentChatLiveTerminalRegistry {
return liveTerminalRegistry ?: project.serviceAsync<AgentChatLiveTerminalRegistryService>()
private suspend fun resolveLiveTerminalRegistry(): AgentThreadViewLiveTerminalRegistry {
return liveTerminalRegistry ?: project.serviceAsync<AgentThreadViewLiveTerminalRegistryService>()
}
private suspend fun resolveStartupLaunchSpec(startupIntent: AgentChatStartupIntent?): AgentSessionTerminalLaunchSpec {
private suspend fun resolveStartupLaunchSpec(startupIntent: AgentThreadViewStartupIntent?): AgentSessionTerminalLaunchSpec {
return when (startupIntent) {
is AgentChatStartupIntent.NewSession -> resolveNewSessionLaunchSpec(startupIntent)
is AgentThreadViewStartupIntent.NewSession -> resolveNewSessionLaunchSpec(startupIntent)
null -> resolveResumeLaunchSpec()
}
}
private suspend fun resolveNewSessionLaunchSpec(startupIntent: AgentChatStartupIntent.NewSession): AgentSessionTerminalLaunchSpec {
private suspend fun resolveNewSessionLaunchSpec(startupIntent: AgentThreadViewStartupIntent.NewSession): AgentSessionTerminalLaunchSpec {
val resolvedLaunchProfile = resolveLaunchProfile(
launchProfileId = startupIntent.launchProfileId,
requiredProvider = startupIntent.provider,
)
val provider = resolvedLaunchProfile?.provider ?: startupIntent.provider
val launchMode = resolvedLaunchProfile?.launchMode ?: startupIntent.launchMode
val launchTargetId = resolvedLaunchProfile?.launchTargetId ?: startupIntent.launchTargetId
val generationSettings = resolvedLaunchProfile?.generationSettings ?: file.generationSettings
val descriptor = AgentSessionProviders.find(provider)
?: throw IllegalStateException("Missing Agent Chat provider for ${provider.value}")
?: throw IllegalStateException("Missing Agent Thread View provider for ${provider.value}")
if (launchMode !in descriptor.supportedLaunchModes) {
throw IllegalStateException("Unsupported Agent Chat launch mode $launchMode for ${provider.value}")
throw IllegalStateException("Unsupported Agent Thread View launch mode $launchMode for ${provider.value}")
}
return AgentSessionLaunchPlanner.plan(
intent = AgentSessionLaunchIntent(
projectPath = file.projectPath,
projectDirectory = file.projectDirectory,
provider = provider,
operation = AgentSessionLaunchOperation.NEW,
launchMode = launchMode,
launchTargetId = launchTargetId,
surfaceId = resolvedLaunchProfile?.surfaceId ?: startupIntent.surfaceId,
generationSettings = generationSettings,
),
project = project,
@@ -322,18 +379,23 @@ internal class AgentChatFileEditor(
}
private suspend fun resolveResumeLaunchSpec(): AgentSessionTerminalLaunchSpec {
val provider = file.provider ?: throw IllegalStateException("Missing Agent Chat provider for ${file.url}")
val provider = file.provider ?: throw IllegalStateException("Missing Agent Thread View provider for ${file.url}")
val resolvedLaunchProfile = resolveLaunchProfile(
launchProfileId = file.launchProfileId,
requiredProvider = provider,
)
val launchTargetId = resolvedLaunchProfile?.launchTargetId ?: file.launchTargetId
val surfaceId = resolvedLaunchProfile?.surfaceId ?: parseAgentThreadViewSurfaceId(file.surfaceId)
return AgentSessionLaunchPlanner.plan(
intent = AgentSessionLaunchIntent(
projectPath = file.projectPath,
projectDirectory = file.projectDirectory,
provider = provider,
operation = AgentSessionLaunchOperation.RESUME,
sessionId = file.threadId.ifBlank { file.sessionId },
launchMode = resolvedLaunchProfile?.launchMode ?: parseAgentChatLaunchMode(file.launchMode),
launchMode = resolvedLaunchProfile?.launchMode ?: parseAgentThreadViewLaunchMode(file.launchMode),
launchTargetId = launchTargetId,
surfaceId = surfaceId,
generationSettings = resolvedLaunchProfile?.generationSettings ?: file.generationSettings,
),
project = project,
@@ -350,8 +412,29 @@ internal class AgentChatFileEditor(
)
}
private fun resolveCustomContentContext(): AgentThreadViewContentContext? {
val provider = file.provider ?: return null
val startupIntent = file.startupIntent() as? AgentThreadViewStartupIntent.NewSession
val launchProfileId = startupIntent?.launchProfileId ?: file.launchProfileId
val resolvedLaunchProfile = resolveLaunchProfile(
launchProfileId = launchProfileId,
requiredProvider = provider,
)
val requestedSurfaceId = resolvedLaunchProfile?.surfaceId ?: startupIntent?.surfaceId ?: parseAgentThreadViewSurfaceId(file.surfaceId)
val surfaceId = providerDescriptorResolver(provider)?.let { descriptor ->
effectiveAgentSessionSurfaceId(descriptor, requestedSurfaceId)
} ?: requestedSurfaceId ?: AgentSessionSurfaces.TERMINAL
return AgentThreadViewContentContext(
provider = provider,
surfaceId = surfaceId,
launchTargetId = resolvedLaunchProfile?.launchTargetId ?: startupIntent?.launchTargetId ?: file.launchTargetId,
threadIdentity = file.threadIdentity,
threadId = file.threadId.ifBlank { file.threadIdentity },
)
}
private suspend fun attachTerminal(
liveTerminalRegistry: AgentChatLiveTerminalRegistry,
liveTerminalRegistry: AgentThreadViewLiveTerminalRegistry,
startupLaunchSpec: AgentSessionTerminalLaunchSpec,
suppressInitialMessageDispatch: Boolean,
) {
@@ -366,7 +449,7 @@ internal class AgentChatFileEditor(
}
private fun attachTerminalOnEdt(
liveTerminalRegistry: AgentChatLiveTerminalRegistry,
liveTerminalRegistry: AgentThreadViewLiveTerminalRegistry,
startupLaunchSpec: AgentSessionTerminalLaunchSpec,
suppressInitialMessageDispatch: Boolean = false,
) {
@@ -375,7 +458,7 @@ internal class AgentChatFileEditor(
}
ensureCrossProjectDockTargetRegistration()
val deferredStartState = file.deferredStartState
if (deferredStartState?.phase == AgentChatDeferredStartPhase.READY_TO_START) {
if (deferredStartState?.phase == AgentThreadViewDeferredStartPhase.READY_TO_START) {
file.updateDeferredStartState(null)
}
file.clearDeferredStartContent()
@@ -393,7 +476,7 @@ internal class AgentChatFileEditor(
if (file.isPendingThread) {
file.updateRestoreOnRestart(false)
}
val pendingController = AgentChatPendingThreadRefreshController(
val pendingController = AgentThreadViewPendingThreadRefreshController(
file = file,
behavior = behavior,
tabSnapshotWriter = tabSnapshotWriter,
@@ -401,14 +484,14 @@ internal class AgentChatFileEditor(
retryIntervalMs = pendingScopedRefreshRetryIntervalMs,
)
pendingThreadRefreshController = pendingController
val concreteController = AgentChatConcreteThreadRebindController(
val concreteController = AgentThreadViewConcreteThreadRebindController(
file = file,
behavior = behavior,
tabSnapshotWriter = tabSnapshotWriter,
currentTimeProvider = currentTimeProvider,
)
concreteThreadRebindController = concreteController
val messageDispatcher = AgentChatInitialMessageDispatcher(
val messageDispatcher = AgentThreadViewInitialMessageDispatcher(
project = project,
file = file,
behavior = behavior,
@@ -421,31 +504,32 @@ internal class AgentChatFileEditor(
if (!suppressInitialMessageDispatch) {
messageDispatcher.schedule(createdTab)
}
scopedTerminalRefreshController = createAgentChatScopedTerminalRefreshController(file, createdTab, providerDescriptor)
val restoreContextController = AgentChatTerminalRestoreContextController(
scopedTerminalRefreshController = createAgentThreadViewScopedTerminalRefreshController(file, createdTab, providerDescriptor)
val restoreContextController = AgentThreadViewTerminalRestoreContextController(
file = file,
descriptor = providerDescriptor,
parentDisposable = this,
)
terminalRestoreContextController = restoreContextController
restoreContextController.attach(createdTab)
terminalTitleThreadRebindController = createAgentChatTerminalTitleThreadRebindController(
terminalTitleThreadRebindController = createAgentThreadViewTerminalTitleThreadRebindController(
file = file,
tab = createdTab,
tabSnapshotWriter = tabSnapshotWriter,
)
installPendingContextInterceptor(createdTab)
disposeDeferredStartProgressTimers(component)
component.removeAll()
pendingContextPanelInstalled = false
component.add(createdTab.component, BorderLayout.CENTER)
installAgentChatTerminalFileDropSupport(createdTab.component, createdTab, this)
installAgentThreadViewTerminalFileDropSupport(createdTab.component, createdTab, this)
pendingContextPanel?.let(::ensurePendingContextPanelInstalled)
component.revalidate()
component.repaint()
focusTerminalIfRequested(createdTab)
}
private fun focusTerminalIfRequested(createdTab: AgentChatTerminalTab) {
private fun focusTerminalIfRequested(createdTab: AgentThreadViewTerminalTab) {
if (!focusTerminalAfterInitialization) {
return
}
@@ -453,21 +537,21 @@ internal class AgentChatFileEditor(
createdTab.preferredFocusableComponent.requestFocusInWindow()
}
private fun getOrCreatePendingContextPanel(): AgentChatPendingContextPanel {
private fun getOrCreatePendingContextPanel(): AgentThreadViewPendingContextPanel {
pendingContextPanel?.let {
return it
}
return AgentChatPendingContextPanel(file.projectPath).also { panel ->
return AgentThreadViewPendingContextPanel(file.projectPath).also { panel ->
pendingContextPanel = panel
}
}
private fun ensurePendingContextPanelInstalled(panel: AgentChatPendingContextPanel) {
private fun ensurePendingContextPanelInstalled(panel: AgentThreadViewPendingContextPanel) {
if (pendingContextPanelInstalled) {
return
}
component.add(panel.component, BorderLayout.SOUTH)
installAgentChatContextFileDropSupport(panel.component, ::addPendingContextItems, this)
installAgentThreadViewContextFileDropSupport(panel.component, ::addPendingContextItems, this)
pendingContextPanelInstalled = true
component.revalidate()
component.repaint()
@@ -475,7 +559,7 @@ internal class AgentChatFileEditor(
private fun ensureCrossProjectDockTargetRegistration() {
if (crossProjectDockTargetRegistration == null && file.projectPath.isNotBlank()) {
crossProjectDockTargetRegistration = AgentChatCrossProjectDockTargetRegistrar().register(project, file)
crossProjectDockTargetRegistration = AgentThreadViewCrossProjectDockTargetRegistrar().register(project, file)
}
}
@@ -486,8 +570,8 @@ internal class AgentChatFileEditor(
}
return
}
forgetAgentChatTabMetadata(file.tabKey)
AgentChatRestoreNotificationService.reportRestoreFailure(project, file, validationError)
forgetAgentThreadViewTabMetadata(file.tabKey)
AgentThreadViewRestoreNotificationService.reportRestoreFailure(project, file, validationError)
if (!project.isDisposed) {
FileEditorManager.getInstance(project).closeFile(file)
}
@@ -553,11 +637,12 @@ internal class AgentChatFileEditor(
component.showForTests()
}
private fun renderDeferredStartState(state: AgentChatDeferredStartState) {
val deferredContent = if (state.phase == AgentChatDeferredStartPhase.WAITING) file.deferredStartContent() else null
private fun renderDeferredStartState(state: AgentThreadViewDeferredStartState) {
val deferredContent = if (state.phase == AgentThreadViewDeferredStartPhase.WAITING) file.deferredStartContent() else null
if (deferredContent == null) {
file.clearDeferredStartContent()
}
disposeDeferredStartProgressTimers(component)
component.removeAll()
component.add(deferredContent?.component ?: createDeferredStartComponent(state), BorderLayout.CENTER)
component.revalidate()
@@ -565,7 +650,7 @@ internal class AgentChatFileEditor(
}
private fun restartTerminalOnEdt(
liveTerminalRegistry: AgentChatLiveTerminalRegistry,
liveTerminalRegistry: AgentThreadViewLiveTerminalRegistry,
startupLaunchSpec: AgentSessionTerminalLaunchSpec,
replaceRetainedTerminal: Boolean,
): Boolean {
@@ -602,6 +687,8 @@ internal class AgentChatFileEditor(
terminalRestoreContextController?.dispose()
terminalRestoreContextController = null
tab = null
customContentPreferredFocusedComponent = null
disposeDeferredStartProgressTimers(component)
component.removeAll()
pendingContextPanelInstalled = false
if (clearPendingContextPanel) {
@@ -609,11 +696,11 @@ internal class AgentChatFileEditor(
}
}
private fun installPendingContextInterceptor(tab: AgentChatTerminalTab) {
private fun installPendingContextInterceptor(tab: AgentThreadViewTerminalTab) {
tab.addInputInterceptor(this, TerminalInputInterceptor { event -> handlePendingContextInput(tab, event) })
}
private fun handlePendingContextInput(tab: AgentChatTerminalTab, event: KeyEvent): Boolean {
private fun handlePendingContextInput(tab: AgentThreadViewTerminalTab, event: KeyEvent): Boolean {
val panel = pendingContextPanel
if (panel == null || !panel.hasItems() || !isPlainEnter(event)) {
return false
@@ -621,15 +708,15 @@ internal class AgentChatFileEditor(
val promptSuffix = resolvePendingContextPromptSuffix(panel) ?: return true
when (tab.sendPendingContextAndExecute(promptSuffix)) {
AgentChatPendingContextSubmissionResult.SUBMITTED -> panel.clear()
AgentChatPendingContextSubmissionResult.UNAVAILABLE -> {
StatusBar.Info.set(AgentChatBundle.message("chat.pending.context.terminal.unavailable"), project)
AgentThreadViewPendingContextSubmissionResult.SUBMITTED -> panel.clear()
AgentThreadViewPendingContextSubmissionResult.UNAVAILABLE -> {
StatusBar.Info.set(AgentThreadViewBundle.message("thread.view.pending.context.terminal.unavailable"), project)
}
}
return true
}
private fun resolvePendingContextPromptSuffix(panel: AgentChatPendingContextPanel): String? {
private fun resolvePendingContextPromptSuffix(panel: AgentThreadViewPendingContextPanel): String? {
val items = panel.pendingItemsSnapshot()
if (items.isEmpty()) {
return null
@@ -650,11 +737,11 @@ internal class AgentChatFileEditor(
val choice = Messages.showDialog(
project,
AgentChatBundle.message("chat.pending.context.softcap.message", serializedChars, softCapChars),
AgentChatBundle.message("chat.pending.context.softcap.title"),
AgentThreadViewBundle.message("thread.view.pending.context.softcap.message", serializedChars, softCapChars),
AgentThreadViewBundle.message("thread.view.pending.context.softcap.title"),
arrayOf(
AgentChatBundle.message("chat.pending.context.softcap.action.send.full"),
AgentChatBundle.message("chat.pending.context.softcap.action.auto.trim"),
AgentThreadViewBundle.message("thread.view.pending.context.softcap.action.send.full"),
AgentThreadViewBundle.message("thread.view.pending.context.softcap.action.auto.trim"),
CommonBundle.getCancelButtonText(),
),
0,
@@ -695,24 +782,98 @@ internal class AgentChatFileEditor(
cachedTabActionsInitialized = false
}
private fun createDeferredStartComponent(state: AgentChatDeferredStartState): JComponent {
private fun createDeferredStartComponent(state: AgentThreadViewDeferredStartState): JComponent {
val rootPanel = JPanel(GridBagLayout())
val content = JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
border = BorderFactory.createEmptyBorder(16, 16, 16, 16)
border = JBUI.Borders.empty(16)
isOpaque = false
}
content.add(createMessageArea(state.title, bold = true))
if (state.phase == AgentThreadViewDeferredStartPhase.WAITING) {
content.add(createDelayedDeferredStartProgressIcon())
content.add(Box.createVerticalStrut(JBUI.scale(8)))
}
content.add(createMessageArea(state.title).apply {
alignmentX = Component.CENTER_ALIGNMENT
})
val stateMessage = state.message
if (!stateMessage.isNullOrBlank()) {
content.add(Box.createVerticalStrut(8))
content.add(createMessageArea(stateMessage, bold = false))
content.add(Box.createVerticalStrut(JBUI.scale(4)))
content.add(createMessageArea(stateMessage, secondary = true).apply {
alignmentX = Component.CENTER_ALIGNMENT
})
}
return JPanel(BorderLayout()).apply {
add(content, BorderLayout.NORTH)
rootPanel.accessibleContext.accessibleName = buildDeferredStartAccessibleName(state)
rootPanel.add(content)
return rootPanel
}
private fun createDelayedDeferredStartProgressIcon(): JComponent {
val icon = createDeferredStartProgressComponent().apply {
name = DEFERRED_START_PROGRESS_NAME
isVisible = false
suspendDeferredStartProgress()
}
val iconSize = icon.preferredSize
val iconPanel = JPanel(BorderLayout()).apply {
alignmentX = Component.CENTER_ALIGNMENT
isOpaque = false
preferredSize = iconSize
minimumSize = iconSize
maximumSize = Dimension(iconSize.width, iconSize.height)
add(icon, BorderLayout.CENTER)
}
val timer = Timer(DEFERRED_START_PROGRESS_DELAY_MS) {
iconPanel.putClientProperty(DEFERRED_START_PROGRESS_TIMER_PROPERTY, null)
if (iconPanel.parent != null) {
icon.isVisible = true
icon.resumeDeferredStartProgress()
iconPanel.revalidate()
iconPanel.repaint()
}
}.apply {
isRepeats = false
start()
}
iconPanel.putClientProperty(DEFERRED_START_PROGRESS_TIMER_PROPERTY, timer)
return iconPanel
}
}
private fun createMessageArea(text: @Nls String, bold: Boolean): JComponent {
private fun disposeDeferredStartProgressTimers(component: Component) {
if (component is JComponent) {
(component.getClientProperty(DEFERRED_START_PROGRESS_TIMER_PROPERTY) as? Timer)?.stop()
component.putClientProperty(DEFERRED_START_PROGRESS_TIMER_PROPERTY, null)
}
if (component is Container) {
component.components.forEach(::disposeDeferredStartProgressTimers)
}
}
private fun createDeferredStartProgressComponent(): JComponent {
if (ApplicationManager.getApplication() == null) {
return JProgressBar().apply {
isIndeterminate = true
isBorderPainted = false
}
}
return AsyncProcessIcon(DEFERRED_START_PROGRESS_NAME)
}
private fun JComponent.suspendDeferredStartProgress() {
(this as? AsyncProcessIcon)?.suspend()
}
private fun JComponent.resumeDeferredStartProgress() {
(this as? AsyncProcessIcon)?.resume()
}
private fun buildDeferredStartAccessibleName(state: AgentThreadViewDeferredStartState): @Nls String {
val stateMessage = state.message?.takeIf { it.isNotBlank() } ?: return state.title
return "${state.title}. $stateMessage"
}
private fun createMessageArea(text: @Nls String, secondary: Boolean = false): JComponent {
return JTextArea(text).apply {
isEditable = false
isFocusable = false
@@ -720,7 +881,9 @@ private fun createMessageArea(text: @Nls String, bold: Boolean): JComponent {
wrapStyleWord = true
isOpaque = false
border = null
font = if (bold) font.deriveFont(font.style or java.awt.Font.BOLD) else font
if (secondary) {
foreground = UIUtil.getContextHelpForeground()
}
}
}
@@ -729,18 +892,18 @@ private fun isPlainEnter(event: KeyEvent): Boolean {
return event.id == KeyEvent.KEY_PRESSED && event.keyCode == KeyEvent.VK_ENTER && event.modifiersEx == 0
}
private fun shouldBlockTerminalInitialization(state: AgentChatDeferredStartState?): Boolean {
private fun shouldBlockTerminalInitialization(state: AgentThreadViewDeferredStartState?): Boolean {
return when (state?.phase) {
AgentChatDeferredStartPhase.WAITING,
AgentChatDeferredStartPhase.SUCCESS_NO_START,
AgentChatDeferredStartPhase.FAILURE_NO_START,
AgentThreadViewDeferredStartPhase.WAITING,
AgentThreadViewDeferredStartPhase.SUCCESS_NO_START,
AgentThreadViewDeferredStartPhase.FAILURE_NO_START,
-> true
else -> false
}
}
private class AgentChatFileEditorComponent : JPanel(BorderLayout()) {
private class AgentThreadViewFileEditorComponent : JPanel(BorderLayout()) {
private var showingForTests: Boolean = false
private var showingContinuation: CancellableContinuation<Unit>? = null
private var showingListener: HierarchyListener? = null
@@ -754,7 +917,7 @@ private class AgentChatFileEditorComponent : JPanel(BorderLayout()) {
return
}
suspendCancellableCoroutine { continuation ->
showingContinuation?.cancel(CancellationException("Superseded by a new Agent Chat editor showing waiter"))
showingContinuation?.cancel(CancellationException("Superseded by a new Agent Thread View editor showing waiter"))
removeShowingListener()
showingContinuation = continuation
val listener = HierarchyListener { event ->
@@ -777,7 +940,7 @@ private class AgentChatFileEditorComponent : JPanel(BorderLayout()) {
}
fun cancelAwaitShowing() {
showingContinuation?.cancel(CancellationException("Agent Chat editor disposed before it was shown"))
showingContinuation?.cancel(CancellationException("Agent Thread View editor disposed before it was shown"))
showingContinuation = null
removeShowingListener()
}
@@ -804,22 +967,22 @@ private class AgentChatFileEditorComponent : JPanel(BorderLayout()) {
}
internal fun interface AgentChatTabSnapshotWriter {
suspend fun upsert(snapshot: AgentChatTabSnapshot)
internal fun interface AgentThreadViewTabSnapshotWriter {
suspend fun upsert(snapshot: AgentThreadViewTabSnapshot)
}
private object ApplicationAgentChatTabSnapshotWriter : AgentChatTabSnapshotWriter {
private object ApplicationAgentThreadViewTabSnapshotWriter : AgentThreadViewTabSnapshotWriter {
@Suppress("UNUSED_PARAMETER")
override suspend fun upsert(snapshot: AgentChatTabSnapshot) = Unit
override suspend fun upsert(snapshot: AgentThreadViewTabSnapshot) = Unit
}
internal fun interface AgentChatArchivedRestoreHandler {
suspend fun closeAndForget(file: AgentChatVirtualFile)
internal fun interface AgentThreadViewArchivedRestoreHandler {
suspend fun closeAndForget(file: AgentThreadViewVirtualFile)
}
private object ApplicationAgentChatArchivedRestoreHandler : AgentChatArchivedRestoreHandler {
override suspend fun closeAndForget(file: AgentChatVirtualFile) {
closeAndForgetAgentChatsForThread(
private object ApplicationAgentThreadViewArchivedRestoreHandler : AgentThreadViewArchivedRestoreHandler {
override suspend fun closeAndForget(file: AgentThreadViewVirtualFile) {
closeAndForgetAgentThreadViewsForThread(
projectPath = file.projectPath,
threadIdentity = file.threadIdentity,
subAgentId = file.subAgentId,
@@ -827,7 +990,7 @@ private object ApplicationAgentChatArchivedRestoreHandler : AgentChatArchivedRes
}
}
internal fun AgentSessionThread.matchesRestoredAgentChatFile(file: AgentChatVirtualFile): Boolean {
internal fun AgentSessionThread.matchesRestoredAgentThreadViewFile(file: AgentThreadViewVirtualFile): Boolean {
if (provider != file.provider) {
return false
}
@@ -838,18 +1001,18 @@ internal fun AgentSessionThread.matchesRestoredAgentChatFile(file: AgentChatVirt
return subAgents.any { subAgent -> subAgent.id == restoredSubAgentId || subAgent.id == file.threadId }
}
internal fun buildAgentChatEditorTabActionGroup(actions: List<AnAction>): ActionGroup? {
internal fun buildAgentThreadViewEditorTabActionGroup(actions: List<AnAction>): ActionGroup? {
if (actions.isEmpty()) {
return null
}
if (actions.size == 1) {
val singleAction = actions.single()
return singleAction as? ActionGroup ?: DumbAwareAgentChatActionGroup(singleAction)
return singleAction as? ActionGroup ?: DumbAwareAgentThreadViewActionGroup(singleAction)
}
return DumbAwareAgentChatActionGroup(actions)
return DumbAwareAgentThreadViewActionGroup(actions)
}
private class DumbAwareAgentChatActionGroup : DefaultActionGroup, DumbAware {
private class DumbAwareAgentThreadViewActionGroup : DefaultActionGroup, DumbAware {
constructor(vararg actions: AnAction) : super(*actions)
constructor(actions: List<AnAction>) : super(actions)
File diff suppressed because it is too large Load Diff