IJPL-248623 align toolbar new-thread context handling

Main-toolbar new-thread launches now capture the toolbar invocation data at the UI action boundary and reuse it for both inline and direct launches. This keeps focus-based context resolution out of the session service while making the non-inline path attach the same default prompt context as the inline prompt flow.

Oversized direct-launch context is trimmed with the existing prompt context soft-cap policy so toolbar launches stay non-blocking without dropping context entirely.

GitOrigin-RevId: 40d555975ab420a50ffe4eb77de35ebd827bb865
This commit is contained in:
Vladimir Krivosheev
2026-07-09 18:59:26 +00:00
committed by intellij-monorepo-bot
parent bc2e559138
commit 1e97f8d180
14 changed files with 584 additions and 138 deletions
@@ -205,6 +205,26 @@ class PiThemeSupportTest {
)
}
@Test
fun bundledExtensionRegistersTaskFolderToolForPi() {
val entrypoint = readBundledPiExtensionText("agent-workbench-extension.ts")
val control = readBundledPiExtensionText("control.ts")
assertThat(entrypoint).contains(
"import {registerTaskFolderTool, startControlBridge} from \"./control.ts\";",
"registerTaskFolderTool(pi, () => controlBridge);",
)
assertThat(control).contains(
"pi.registerTool(defineTool({",
"name: \"agent_workbench_create_task_folder\"",
"parameters: Type.Object({",
"name: Type.String({description: \"Task folder name\"})",
"issue: Type.Optional(Type.String({description: \"Issue tracker id to store as task folder metadata key 'issue'\"}))",
"const result = await bridge.createAndAssignTaskFolder(params.name, metadata);",
"details: result",
)
}
private fun supportFor(
extension: PiBundledExtensionResources,
themeSnapshotProvider: () -> PiThemeSnapshot = { snapshot("islands-dark", "Islands Dark", dark = true) },
@@ -16,12 +16,81 @@ private val LOG = logger<AgentPromptContextEnvelopeFormatterLog>()
object AgentPromptContextEnvelopeFormatter {
const val DEFAULT_SOFT_CAP_CHARS: Int = 12_000
data class ContextEnvelopeSelection(
@JvmField val items: List<AgentPromptContextItem>,
@JvmField val summary: AgentPromptContextEnvelopeSummary,
@JvmField val serializedChars: Int,
@JvmField val exceedsSoftCap: Boolean,
)
data class SoftCapTrimResult(
@JvmField val items: List<AgentPromptContextItem>,
@JvmField val serializedChars: Int,
@JvmField val exceedsSoftCap: Boolean,
)
fun prepareContextEnvelopeSelection(
items: List<AgentPromptContextItem>,
softCapChars: Int = DEFAULT_SOFT_CAP_CHARS,
projectPath: String? = null,
): ContextEnvelopeSelection {
val summary = AgentPromptContextEnvelopeSummary(
softCapChars = softCapChars,
softCapExceeded = false,
autoTrimApplied = false,
)
if (items.isEmpty()) {
return ContextEnvelopeSelection(
items = emptyList(),
summary = summary,
serializedChars = 0,
exceedsSoftCap = false,
)
}
val normalizedItems = items.map { item -> normalizeItem(item) }
val serializedChars = measureContextBlockChars(
items = normalizedItems,
summary = summary,
projectPath = projectPath,
)
return ContextEnvelopeSelection(
items = normalizedItems,
summary = summary,
serializedChars = serializedChars,
exceedsSoftCap = serializedChars > softCapChars,
)
}
fun markSoftCapExceeded(selection: ContextEnvelopeSelection): ContextEnvelopeSelection {
return selection.copy(
summary = selection.summary.copy(
softCapExceeded = true,
autoTrimApplied = false,
),
exceedsSoftCap = true,
)
}
fun autoTrimContextEnvelopeSelection(
selection: ContextEnvelopeSelection,
projectPath: String? = null,
): ContextEnvelopeSelection {
val trimResult = applySoftCap(
items = selection.items,
softCapChars = selection.summary.softCapChars,
projectPath = projectPath,
)
return ContextEnvelopeSelection(
items = trimResult.items,
summary = selection.summary.copy(
softCapExceeded = true,
autoTrimApplied = true,
),
serializedChars = trimResult.serializedChars,
exceedsSoftCap = trimResult.exceedsSoftCap,
)
}
fun composeInitialMessage(request: AgentPromptInitialMessageRequest): String {
val prompt = request.prompt.trim()
if (request.contextItems.isEmpty()) {
@@ -0,0 +1,126 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.agent.workbench.prompt.core
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Timeout
import java.util.concurrent.TimeUnit
@Timeout(value = 2, unit = TimeUnit.MINUTES)
class AgentPromptContextEnvelopeFormatterTest {
@Test
fun contextEnvelopePreparationKeepsEmptyContextBelowSoftCap() {
val selection = AgentPromptContextEnvelopeFormatter.prepareContextEnvelopeSelection(
items = emptyList(),
softCapChars = 1,
)
assertThat(selection.items).isEmpty()
assertThat(selection.serializedChars).isEqualTo(0)
assertThat(selection.exceedsSoftCap).isFalse()
}
@Test
fun contextEnvelopePreparationNormalizesItemsAndReportsSoftCapState() {
val selection = AgentPromptContextEnvelopeFormatter.prepareContextEnvelopeSelection(
items = listOf(contextItem(rendererId = AgentPromptContextRendererIds.SNIPPET, title = "Selection", body = " selected code ")),
softCapChars = 1_000,
projectPath = "/work/repo",
)
assertThat(selection.items.single().body).isEqualTo("selected code")
assertThat(selection.summary.softCapChars).isEqualTo(1_000)
assertThat(selection.summary.softCapExceeded).isFalse()
assertThat(selection.summary.autoTrimApplied).isFalse()
assertThat(selection.exceedsSoftCap).isFalse()
}
@Test
fun contextEnvelopePreparationCanMarkFullOversizedSelectionWithoutTrimming() {
val oversizedBody = "x".repeat(1_000)
val selection = AgentPromptContextEnvelopeFormatter.prepareContextEnvelopeSelection(
items = listOf(contextItem(rendererId = AgentPromptContextRendererIds.SNIPPET, title = "Selection", body = oversizedBody)),
softCapChars = 400,
projectPath = "/work/repo",
)
val marked = AgentPromptContextEnvelopeFormatter.markSoftCapExceeded(selection)
assertThat(marked.items.single().body).isEqualTo(oversizedBody)
assertThat(marked.summary.softCapExceeded).isTrue()
assertThat(marked.summary.autoTrimApplied).isFalse()
assertThat(marked.exceedsSoftCap).isTrue()
}
@Test
fun contextEnvelopePreparationAutoTrimsOversizedSelection() {
val oversizedBody = "x".repeat(AgentPromptContextEnvelopeFormatter.DEFAULT_SOFT_CAP_CHARS * 2)
val selection = AgentPromptContextEnvelopeFormatter.prepareContextEnvelopeSelection(
items = listOf(contextItem(rendererId = AgentPromptContextRendererIds.SNIPPET, title = "Selection", body = oversizedBody)),
projectPath = "/work/repo",
)
val trimmed = AgentPromptContextEnvelopeFormatter.autoTrimContextEnvelopeSelection(
selection = selection,
projectPath = "/work/repo",
)
assertThat(trimmed.summary.softCapExceeded).isTrue()
assertThat(trimmed.summary.autoTrimApplied).isTrue()
assertThat(trimmed.items).isNotEmpty()
assertThat(trimmed.items.joinToString("\n") { item -> item.body }).isNotEqualTo(oversizedBody)
assertThat(trimmed.serializedChars).isLessThanOrEqualTo(AgentPromptContextEnvelopeFormatter.DEFAULT_SOFT_CAP_CHARS)
assertThat(trimmed.exceedsSoftCap).isFalse()
}
@Test
fun softCapTrimmingStartsFromLastContextItem() {
val first = contextItem(rendererId = AgentPromptContextRendererIds.SNIPPET, title = "First", body = "a".repeat(420))
val second = contextItem(rendererId = AgentPromptContextRendererIds.PATHS, title = "Second", body = "b".repeat(420))
val result = AgentPromptContextEnvelopeFormatter.applySoftCap(
items = listOf(first, second),
softCapChars = 700,
)
assertThat(result.items).hasSize(2)
assertThat(result.items.first().body).isEqualTo(first.body)
assertThat(result.items.last().truncation.reason).isIn(
AgentPromptContextTruncationReason.SOFT_CAP_PARTIAL,
AgentPromptContextTruncationReason.SOFT_CAP_OMITTED,
)
assertThat(result.serializedChars).isLessThanOrEqualTo(700)
assertThat(result.exceedsSoftCap).isFalse()
}
@Test
fun verySmallSoftCapKeepsItemStubsInsteadOfDroppingItems() {
val result = AgentPromptContextEnvelopeFormatter.applySoftCap(
items = listOf(
contextItem(rendererId = AgentPromptContextRendererIds.SNIPPET, title = "A", body = "x".repeat(200)),
contextItem(rendererId = AgentPromptContextRendererIds.PATHS, title = "B", body = "y".repeat(200)),
),
softCapChars = 200,
)
assertThat(result.items).hasSize(2)
assertThat(result.items.count { item -> item.body == "[omitted due to soft cap]" }).isGreaterThan(0)
assertThat(result.items)
.allSatisfy { item ->
assertThat(item.truncation.reason).isNotEqualTo(AgentPromptContextTruncationReason.NONE)
}
}
private fun contextItem(
rendererId: String,
title: String,
body: String,
): AgentPromptContextItem {
return AgentPromptContextItem(
rendererId = rendererId,
title = title,
body = body,
source = "test",
)
}
}
@@ -202,28 +202,18 @@ internal class AgentPromptPaletteContextController(
}
fun resolveContextSelection(items: List<AgentPromptContextItem>, projectPath: String?): AgentPromptPaletteContextSelection? {
val baseSummary = AgentPromptContextEnvelopeSummary(
val selection = AgentPromptContextEnvelopeFormatter.prepareContextEnvelopeSelection(
items = items,
softCapChars = CONTEXT_SOFT_CAP_CHARS,
softCapExceeded = false,
autoTrimApplied = false,
)
if (items.isEmpty()) {
return AgentPromptPaletteContextSelection(items = emptyList(), summary = baseSummary)
}
val normalizedItems = items.map { item -> item.copy(body = item.body.trim()) }
val serializedChars = AgentPromptContextEnvelopeFormatter.measureContextBlockChars(
items = normalizedItems,
summary = baseSummary,
projectPath = projectPath,
)
if (serializedChars <= CONTEXT_SOFT_CAP_CHARS) {
return AgentPromptPaletteContextSelection(items = normalizedItems, summary = baseSummary)
if (!selection.exceedsSoftCap) {
return AgentPromptPaletteContextSelection(items = selection.items, summary = selection.summary)
}
val choice = Messages.showDialog(
project,
AgentPromptBundle.message("popup.context.softcap.message", serializedChars, CONTEXT_SOFT_CAP_CHARS),
AgentPromptBundle.message("popup.context.softcap.message", selection.serializedChars, CONTEXT_SOFT_CAP_CHARS),
AgentPromptBundle.message("popup.context.softcap.title"),
arrayOf(
AgentPromptBundle.message("popup.context.softcap.action.send.full"),
@@ -235,30 +225,12 @@ internal class AgentPromptPaletteContextController(
)
return when (choice) {
0 -> AgentPromptPaletteContextSelection(
items = normalizedItems,
summary = AgentPromptContextEnvelopeSummary(
softCapChars = CONTEXT_SOFT_CAP_CHARS,
softCapExceeded = true,
autoTrimApplied = false,
),
)
0 -> AgentPromptContextEnvelopeFormatter.markSoftCapExceeded(selection).toPaletteContextSelection()
1 -> {
val trimResult = AgentPromptContextEnvelopeFormatter.applySoftCap(
items = normalizedItems,
softCapChars = CONTEXT_SOFT_CAP_CHARS,
projectPath = projectPath,
)
AgentPromptPaletteContextSelection(
items = trimResult.items,
summary = AgentPromptContextEnvelopeSummary(
softCapChars = CONTEXT_SOFT_CAP_CHARS,
softCapExceeded = true,
autoTrimApplied = true,
),
)
}
1 -> AgentPromptContextEnvelopeFormatter.autoTrimContextEnvelopeSelection(
selection = selection,
projectPath = projectPath,
).toPaletteContextSelection()
else -> null
}
@@ -358,3 +330,7 @@ internal data class AgentPromptPaletteContextSelection(
@JvmField val items: List<AgentPromptContextItem>,
@JvmField val summary: AgentPromptContextEnvelopeSummary,
)
private fun AgentPromptContextEnvelopeFormatter.ContextEnvelopeSelection.toPaletteContextSelection(): AgentPromptPaletteContextSelection {
return AgentPromptPaletteContextSelection(items = items, summary = summary)
}
@@ -1,65 +0,0 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.agent.workbench.prompt.ui
import com.intellij.agent.workbench.prompt.core.AgentPromptContextEnvelopeFormatter
import com.intellij.agent.workbench.prompt.core.AgentPromptContextItem
import com.intellij.agent.workbench.prompt.core.AgentPromptContextRendererIds
import com.intellij.agent.workbench.prompt.core.AgentPromptContextTruncationReason
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Timeout
import java.util.concurrent.TimeUnit
@Timeout(value = 2, unit = TimeUnit.MINUTES)
class AgentPromptContextSoftCapPolicyTest {
@Test
fun softCapTrimmingStartsFromLastContextItem() {
val first = contextItem(rendererId = AgentPromptContextRendererIds.SNIPPET, title = "First", body = "a".repeat(420))
val second = contextItem(rendererId = AgentPromptContextRendererIds.PATHS, title = "Second", body = "b".repeat(420))
val result = AgentPromptContextEnvelopeFormatter.applySoftCap(
items = listOf(first, second),
softCapChars = 700,
)
assertThat(result.items).hasSize(2)
assertThat(result.items.first().body).isEqualTo(first.body)
assertThat(result.items.last().truncation.reason).isIn(
AgentPromptContextTruncationReason.SOFT_CAP_PARTIAL,
AgentPromptContextTruncationReason.SOFT_CAP_OMITTED,
)
assertThat(result.serializedChars).isLessThanOrEqualTo(700)
assertThat(result.exceedsSoftCap).isFalse()
}
@Test
fun verySmallSoftCapKeepsItemStubsInsteadOfDroppingItems() {
val result = AgentPromptContextEnvelopeFormatter.applySoftCap(
items = listOf(
contextItem(rendererId = AgentPromptContextRendererIds.SNIPPET, title = "A", body = "x".repeat(200)),
contextItem(rendererId = AgentPromptContextRendererIds.PATHS, title = "B", body = "y".repeat(200)),
),
softCapChars = 200,
)
assertThat(result.items).hasSize(2)
assertThat(result.items.count { item -> item.body == "[omitted due to soft cap]" }).isGreaterThan(0)
assertThat(result.items)
.allSatisfy { item ->
assertThat(item.truncation.reason).isNotEqualTo(AgentPromptContextTruncationReason.NONE)
}
}
private fun contextItem(
rendererId: String,
title: String,
body: String,
): AgentPromptContextItem {
return AgentPromptContextItem(
rendererId = rendererId,
title = title,
body = body,
source = "test",
)
}
}
@@ -5,6 +5,8 @@ package com.intellij.agent.workbench.sessions.actions
// @spec community/plugins/agent-workbench/spec/actions/global-prompt-task-cost-profiles.spec.md
import com.intellij.agent.workbench.prompt.core.AgentPromptLaunchProfile
import com.intellij.agent.workbench.prompt.core.AGENT_PROMPT_INVOCATION_DATA_CONTEXT_KEY
import com.intellij.agent.workbench.prompt.core.AgentPromptInvocationData
import com.intellij.agent.workbench.sessions.AgentSessionLaunchProfileMenuItem
import com.intellij.agent.workbench.sessions.AgentSessionLaunchProfileSelection
import com.intellij.agent.workbench.sessions.AgentSessionsBundle
@@ -25,6 +27,7 @@ import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProvid
import com.intellij.platform.ai.agent.sessions.core.providers.hasEntries
import com.intellij.agent.workbench.sessions.statistics.AgentWorkbenchEntryPoint
import com.intellij.agent.workbench.sessions.state.AgentSessionUiPreferencesStateService
import com.intellij.agent.workbench.ui.AgentWorkbenchActionIds
import com.intellij.agent.workbench.ui.AgentWorkbenchPopupRow
import com.intellij.agent.workbench.ui.createAgentWorkbenchListPopup
import com.intellij.icons.AllIcons
@@ -50,6 +53,22 @@ import org.jetbrains.annotations.TestOnly
import java.awt.Dimension
import javax.swing.JComponent
internal typealias ToolbarNewThreadLauncher = (
String,
AgentPromptLaunchProfile,
Project,
AgentWorkbenchEntryPoint,
AgentPromptInvocationData?,
) -> Unit
private fun adaptLegacyNewThreadLauncher(
createNewSession: (String, AgentPromptLaunchProfile, Project, AgentWorkbenchEntryPoint) -> Unit,
): ToolbarNewThreadLauncher {
return { path, profile, project, entryPoint, _ ->
createNewSession(path, profile, project, entryPoint)
}
}
/**
* Single split-button entry on `MainToolbarRight` that exposes "New Thread":
* the icon shows the effective launch profile provider+mode badge, falling back to the first available profile when no default exists yet.
@@ -61,7 +80,7 @@ import javax.swing.JComponent
internal class AgentSessionsMainToolbarNewThreadAction private constructor(
private val resolveContext: (AnActionEvent) -> AgentSessionsNewThreadContext?,
private val allBridges: () -> List<AgentSessionProviderDescriptor>,
createNewSession: (String, AgentPromptLaunchProfile, Project, AgentWorkbenchEntryPoint) -> Unit,
createNewSession: ToolbarNewThreadLauncher,
private val userLaunchProfiles: () -> List<AgentPromptLaunchProfile>,
private val defaultLaunchProfileId: () -> String?,
pickerGroup: ProfilePickerActionGroup,
@@ -86,20 +105,21 @@ internal class AgentSessionsMainToolbarNewThreadAction private constructor(
resolveAgentSessionsMainToolbarNewThreadContext(event)
},
allBridges: () -> List<AgentSessionProviderDescriptor> = AgentSessionProviders::allProviders,
createNewSession: (String, AgentPromptLaunchProfile, Project, AgentWorkbenchEntryPoint) -> Unit = ::createNewThreadViaService,
createNewSession: ((String, AgentPromptLaunchProfile, Project, AgentWorkbenchEntryPoint) -> Unit)? = null,
createNewSessionWithInvocationData: ToolbarNewThreadLauncher = ::createNewThreadViaService,
userLaunchProfiles: () -> List<AgentPromptLaunchProfile> = { service<AgentSessionUiPreferencesStateService>().getUserLaunchProfiles() },
defaultLaunchProfileId: () -> String? = { service<AgentSessionUiPreferencesStateService>().getDefaultLaunchProfileId() },
showPicker: (ActionGroup, AnActionEvent) -> Unit = ::showToolbarProfilePicker,
) : this(
resolveContext = resolveContext,
allBridges = allBridges,
createNewSession = createNewSession,
createNewSession = createNewSession?.let(::adaptLegacyNewThreadLauncher) ?: createNewSessionWithInvocationData,
userLaunchProfiles = userLaunchProfiles,
defaultLaunchProfileId = defaultLaunchProfileId,
pickerGroup = ProfilePickerActionGroup(
resolveContext = resolveContext,
allBridges = allBridges,
createNewSession = createNewSession,
createNewSession = createNewSession?.let(::adaptLegacyNewThreadLauncher) ?: createNewSessionWithInvocationData,
userLaunchProfiles = userLaunchProfiles,
defaultLaunchProfileId = defaultLaunchProfileId,
entryPoint = AgentWorkbenchEntryPoint.TOOLBAR,
@@ -155,7 +175,7 @@ internal class AgentSessionsMainToolbarNewThreadAction private constructor(
private class ProfilePickerActionGroup(
private val resolveContext: (AnActionEvent) -> AgentSessionsNewThreadContext?,
private val allBridges: () -> List<AgentSessionProviderDescriptor>,
private val createNewSession: (String, AgentPromptLaunchProfile, Project, AgentWorkbenchEntryPoint) -> Unit,
private val createNewSession: ToolbarNewThreadLauncher,
private val userLaunchProfiles: () -> List<AgentPromptLaunchProfile>,
private val defaultLaunchProfileId: () -> String?,
private val entryPoint: AgentWorkbenchEntryPoint = AgentWorkbenchEntryPoint.TOOLBAR,
@@ -168,13 +188,14 @@ private class ProfilePickerActionGroup(
override fun getChildren(e: AnActionEvent?): Array<AnAction> {
val content = e?.let(::resolveContent) ?: return emptyArray()
val launchNewThread = createEventAwareNewThreadLauncher(e, content.context)
return when (val target = content.target) {
is AgentSessionsNewThreadTarget.Direct -> buildAgentSessionLaunchProfileMenuActions(
path = target.path,
project = content.context.project,
selection = content.selection,
entryPoint = entryPoint,
createNewSession = createNewSession,
createNewSession = launchNewThread,
)
is AgentSessionsNewThreadTarget.Candidates -> target.candidates.mapTo(mutableListOf<AnAction>()) { candidate ->
DefaultActionGroup(candidate.displayName, true).apply {
@@ -183,7 +204,7 @@ private class ProfilePickerActionGroup(
project = content.context.project,
selection = content.selection,
entryPoint = entryPoint,
createNewSession = createNewSession,
createNewSession = launchNewThread,
includeManageAction = false,
).forEach(::add)
}
@@ -193,13 +214,14 @@ private class ProfilePickerActionGroup(
fun createRows(e: AnActionEvent): List<AgentWorkbenchPopupRow> {
val content = resolveContent(e) ?: return emptyList()
val launchNewThread = createEventAwareNewThreadLauncher(e, content.context)
return when (val target = content.target) {
is AgentSessionsNewThreadTarget.Direct -> buildAgentSessionLaunchProfileMenuRows(
path = target.path,
project = content.context.project,
selection = content.selection,
entryPoint = entryPoint,
createNewSession = createNewSession,
createNewSession = launchNewThread,
event = e,
)
is AgentSessionsNewThreadTarget.Candidates -> target.candidates.map { candidate ->
@@ -212,7 +234,7 @@ private class ProfilePickerActionGroup(
project = content.context.project,
selection = content.selection,
entryPoint = entryPoint,
createNewSession = createNewSession,
createNewSession = launchNewThread,
includeManageAction = false,
event = e,
),
@@ -221,6 +243,16 @@ private class ProfilePickerActionGroup(
}
}
private fun createEventAwareNewThreadLauncher(
e: AnActionEvent,
context: AgentSessionsNewThreadContext,
): (String, AgentPromptLaunchProfile, Project, AgentWorkbenchEntryPoint) -> Unit {
val invocationData = createToolbarNewThreadInvocationData(e, context.project)
return { path, profile, project, entryPoint ->
createNewSession(path, profile, project, entryPoint, invocationData)
}
}
private fun resolveContent(e: AnActionEvent): ProfilePickerContent? {
val context = resolveContext(e) ?: return null
val target = context.target ?: return null
@@ -241,7 +273,7 @@ private class ProfilePickerActionGroup(
internal class ProfileQuickStartAction(
private val resolveContext: (AnActionEvent) -> AgentSessionsNewThreadContext?,
private val allBridges: () -> List<AgentSessionProviderDescriptor>,
private val createNewSession: (String, AgentPromptLaunchProfile, Project, AgentWorkbenchEntryPoint) -> Unit,
private val createNewSession: ToolbarNewThreadLauncher,
private val userLaunchProfiles: () -> List<AgentPromptLaunchProfile>,
private val defaultLaunchProfileId: () -> String?,
private val entryPoint: AgentWorkbenchEntryPoint,
@@ -278,7 +310,13 @@ internal class ProfileQuickStartAction(
showPicker(pickerGroup, e)
return
}
createNewSession(target.path, quickStart.profile, context.project, entryPoint)
createNewSession(
target.path,
quickStart.profile,
context.project,
entryPoint,
createToolbarNewThreadInvocationData(e, context.project),
)
}
is AgentSessionsNewThreadTarget.Candidates, null -> showPicker(pickerGroup, e)
}
@@ -533,3 +571,19 @@ private fun showToolbarPopup(popup: ListPopup, e: AnActionEvent) {
popup.showInBestPositionFor(e.dataContext)
}
}
private fun createToolbarNewThreadInvocationData(
e: AnActionEvent,
project: Project,
): AgentPromptInvocationData {
return AgentPromptInvocationData(
project = project,
actionId = AgentWorkbenchActionIds.Sessions.MainToolbar.NEW_THREAD,
actionText = e.presentation.text,
actionPlace = e.place,
invokedAtMs = System.currentTimeMillis(),
attributes = mapOf(
AGENT_PROMPT_INVOCATION_DATA_CONTEXT_KEY to e.dataContext,
),
)
}
@@ -5,6 +5,10 @@ package com.intellij.agent.workbench.sessions.actions
// @spec community/plugins/agent-workbench/spec/actions/new-thread.spec.md
import com.intellij.agent.workbench.chat.AgentChatDeferredStartContent
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
@@ -46,11 +50,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)
createNewThreadDirectly(
path = path,
profile = profile,
currentProject = currentProject,
entryPoint = entryPoint,
invocationData = invocationData,
)
return
}
service<AgentSessionsInlineNewThreadPromptService>().openInlinePrompt(
@@ -58,6 +84,7 @@ fun createNewThreadViaService(
profile = profile,
currentProject = currentProject,
entryPoint = entryPoint,
invocationData = invocationData,
)
}
@@ -78,16 +105,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,
@@ -97,6 +168,7 @@ internal class AgentSessionsInlineNewThreadPromptService internal constructor(
profile: AgentPromptLaunchProfile,
currentProject: Project,
entryPoint: AgentWorkbenchEntryPoint,
invocationData: AgentPromptInvocationData?,
) {
coroutineScope.launch(CoroutineName("Agent Workbench inline New Thread prompt")) {
openInlinePromptSuspending(
@@ -104,6 +176,7 @@ internal class AgentSessionsInlineNewThreadPromptService internal constructor(
profile = profile,
currentProject = currentProject,
entryPoint = entryPoint,
invocationData = invocationData,
)
}
}
@@ -113,6 +186,7 @@ internal class AgentSessionsInlineNewThreadPromptService internal constructor(
profile: AgentPromptLaunchProfile,
currentProject: Project,
entryPoint: AgentWorkbenchEntryPoint,
invocationData: AgentPromptInvocationData?,
) {
val normalizedPath = normalizeAgentWorkbenchPath(path)
val provider = AgentSessionProvider.from(profile.providerId)
@@ -133,6 +207,7 @@ internal class AgentSessionsInlineNewThreadPromptService internal constructor(
profile = profile,
entryPoint = entryPoint,
handleDeferred = handleDeferred,
invocationData = invocationData,
)
},
).handle
@@ -144,13 +219,25 @@ internal class AgentSessionsInlineNewThreadPromptService internal constructor(
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) {
handleDeferred.cancel()
createNewThreadDirectly(path = normalizedPath, profile = profile, currentProject = currentProject, entryPoint = entryPoint)
createNewThreadDirectly(
path = normalizedPath,
profile = profile,
currentProject = currentProject,
entryPoint = entryPoint,
invocationData = invocationData,
)
return
}
handleDeferred.complete(handle)
@@ -162,17 +249,12 @@ internal class AgentSessionsInlineNewThreadPromptService internal constructor(
profile: AgentPromptLaunchProfile,
entryPoint: AgentWorkbenchEntryPoint,
handleDeferred: CompletableDeferred<AgentDeferredNewSessionHandle>,
invocationData: AgentPromptInvocationData?,
): AgentChatDeferredStartContent {
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)).copy(project = project),
launcherProvider = { launcher },
initialLaunchProfileId = profile.id,
)
@@ -184,6 +266,16 @@ internal class AgentSessionsInlineNewThreadPromptService internal constructor(
}
}
private fun defaultNewThreadInvocationData(project: Project, entryPoint: AgentWorkbenchEntryPoint): AgentPromptInvocationData {
return AgentPromptInvocationData(
project = project,
actionId = AgentWorkbenchActionIds.Sessions.MainToolbar.NEW_THREAD,
actionText = AgentSessionsBundle.message("action.AgentWorkbenchSessions.MainToolbar.NewThread.text"),
actionPlace = entryPoint.name,
invokedAtMs = System.currentTimeMillis(),
)
}
private class InlineNewThreadPromptLauncherBridge(
private val projectPath: String,
private val handleProvider: suspend () -> AgentDeferredNewSessionHandle,
@@ -6,16 +6,22 @@ import com.intellij.agent.workbench.ui.AgentWorkbenchActionIds
import com.intellij.platform.ai.agent.core.normalizeAgentWorkbenchPath
import com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode
import com.intellij.platform.ai.agent.core.session.AgentSessionProvider
import com.intellij.agent.workbench.prompt.core.AgentPromptContextEnvelopeFormatter
import com.intellij.agent.workbench.prompt.core.AgentPromptContextItem
import com.intellij.agent.workbench.prompt.core.AgentPromptContextRendererIds
import com.intellij.agent.workbench.prompt.core.AgentPromptGenerationSettings
import com.intellij.agent.workbench.prompt.core.AgentPromptInvocationData
import com.intellij.agent.workbench.prompt.core.AgentPromptLaunchProfile
import com.intellij.agent.workbench.prompt.core.AgentPromptLaunchProfileKind
import com.intellij.agent.workbench.prompt.core.AgentPromptProjectPathCandidate
import com.intellij.agent.workbench.prompt.core.AgentPromptReasoningEffort
import com.intellij.agent.workbench.prompt.core.dataContextOrNull
import com.intellij.agent.workbench.sessions.actions.AgentSessionsDirectPathNewThreadAction
import com.intellij.agent.workbench.sessions.actions.AgentSessionsNewThreadContext
import com.intellij.agent.workbench.sessions.actions.AgentSessionsNewThreadTarget
import com.intellij.agent.workbench.sessions.actions.AgentSessionsMainToolbarNewThreadAction
import com.intellij.agent.workbench.sessions.actions.ProfileQuickStartAction
import com.intellij.agent.workbench.sessions.actions.buildNewThreadInitialMessageRequest
import com.intellij.agent.workbench.sessions.actions.resolveAgentSessionsMainToolbarNewThreadContext
import com.intellij.agent.workbench.sessions.actions.resolveQuickStartProjectPopupAnchor
import com.intellij.agent.workbench.sessions.actions.shouldOpenInlineNewThreadPrompt
@@ -266,6 +272,34 @@ class AgentSessionsMainToolbarNewThreadActionsTest {
assertThat(activeProfileId).isEqualTo(builtInLaunchProfileId(AgentSessionProvider.from("codex"), AgentSessionLaunchMode.YOLO))
}
@Test
fun mainToolbarQuickStartPassesActionDataContextToInvocationData() {
val context = newThreadContext(path = "/tmp/toolbar-project")
val dataContext = SimpleDataContext.builder()
.add(CommonDataKeys.PROJECT, context.project)
.build()
var capturedInvocationData: AgentPromptInvocationData? = null
val codexBridge = TestAgentSessionProviderDescriptor(
provider = AgentSessionProvider.from("codex"),
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
cliAvailable = true,
)
val action = AgentSessionsMainToolbarNewThreadAction(
resolveContext = { context },
allBridges = { listOf(codexBridge) },
createNewSessionWithInvocationData = { _, _, _, _, invocationData ->
capturedInvocationData = invocationData
},
defaultLaunchProfileId = { builtInLaunchProfileId(AgentSessionProvider.from("codex"), AgentSessionLaunchMode.STANDARD) },
)
val mainAction = checkNotNull(action.getMainAction(TestActionEvent.createTestEvent(action, dataContext)))
mainAction.actionPerformed(TestActionEvent.createTestEvent(mainAction, dataContext))
assertThat(capturedInvocationData?.actionId).isEqualTo(AgentWorkbenchActionIds.Sessions.MainToolbar.NEW_THREAD)
assertThat(capturedInvocationData?.dataContextOrNull()).isSameAs(dataContext)
}
@Test
fun getMainActionLaunchesExplicitDefaultUserProfile() {
val context = newThreadContext(path = "/tmp/toolbar-project")
@@ -627,6 +661,37 @@ class AgentSessionsMainToolbarNewThreadActionsTest {
assertThat(launchedProfile?.kind).isEqualTo(AgentPromptLaunchProfileKind.BUILT_IN)
}
@Test
fun mainToolbarPickerRowPassesActionDataContextToInvocationData() {
val context = newThreadContext(path = "/tmp/repo-direct")
val dataContext = SimpleDataContext.builder()
.add(CommonDataKeys.PROJECT, context.project)
.build()
var capturedInvocationData: AgentPromptInvocationData? = null
val codexBridge = TestAgentSessionProviderDescriptor(
provider = AgentSessionProvider.from("codex"),
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
cliAvailable = true,
)
val action = AgentSessionsMainToolbarNewThreadAction(
resolveContext = { context },
allBridges = { listOf(codexBridge) },
createNewSessionWithInvocationData = { _, _, _, _, invocationData ->
capturedInvocationData = invocationData
},
defaultLaunchProfileId = { builtInLaunchProfileId(AgentSessionProvider.from("codex"), AgentSessionLaunchMode.STANDARD) },
)
val event = TestActionEvent.createTestEvent(action, dataContext)
val row = action.createProfilePickerRowsForTest(event).single { popupRow ->
popupRow.text == AgentSessionsBundle.message("toolwindow.action.new.session.codex")
}
checkNotNull(row.onChosen).invoke()
assertThat(capturedInvocationData?.actionId).isEqualTo(AgentWorkbenchActionIds.Sessions.MainToolbar.NEW_THREAD)
assertThat(capturedInvocationData?.dataContextOrNull()).isSameAs(dataContext)
}
@Test
fun mainToolbarPickerSelectionDoesNotChangeDefaultProfileMarking() {
val context = newThreadContext(path = "/tmp/repo-direct")
@@ -857,6 +922,62 @@ class AgentSessionsMainToolbarNewThreadActionsTest {
assertThat(request.providerOptionIds).isEmpty()
}
@Test
fun nonInlineNewThreadInitialMessageRequestIncludesCollectedInvocationContext() {
val profile = AgentPromptLaunchProfile(
id = "user:context",
name = "Context Codex",
providerId = AgentSessionProvider.from("codex").value,
)
val invocationData = invocationData()
var collectedInvocationData: AgentPromptInvocationData? = null
val request = buildNewThreadInitialMessageRequest(
profile = profile,
projectPath = "/work/repo",
invocationData = invocationData,
collectDefaultContext = { data ->
collectedInvocationData = data
listOf(contextItem(body = " selected code "))
},
)
assertThat(collectedInvocationData).isSameAs(invocationData)
assertThat(request.prompt).isEmpty()
assertThat(request.projectPath).isEqualTo("/work/repo")
assertThat(request.contextItems.single().body).isEqualTo("selected code")
assertThat(request.contextEnvelopeSummary?.softCapExceeded).isFalse()
assertThat(request.contextEnvelopeSummary?.autoTrimApplied).isFalse()
}
@Test
fun nonInlineNewThreadInitialMessageRequestAutoTrimsOversizedInvocationContext() {
val profile = AgentPromptLaunchProfile(
id = "user:large-context",
name = "Large Context Codex",
providerId = AgentSessionProvider.from("codex").value,
)
val oversizedBody = "x".repeat(AgentPromptContextEnvelopeFormatter.DEFAULT_SOFT_CAP_CHARS * 2)
val request = buildNewThreadInitialMessageRequest(
profile = profile,
projectPath = "/work/repo",
invocationData = invocationData(),
collectDefaultContext = { listOf(contextItem(body = oversizedBody)) },
)
val summary = checkNotNull(request.contextEnvelopeSummary)
assertThat(summary.softCapExceeded).isTrue()
assertThat(summary.autoTrimApplied).isTrue()
assertThat(request.contextItems).isNotEmpty()
assertThat(request.contextItems.joinToString("\n") { item -> item.body }).isNotEqualTo(oversizedBody)
assertThat(AgentPromptContextEnvelopeFormatter.measureContextBlockChars(
items = request.contextItems,
summary = summary,
projectPath = request.projectPath,
)).isLessThanOrEqualTo(AgentPromptContextEnvelopeFormatter.DEFAULT_SOFT_CAP_CHARS)
}
@Test
fun inlineNewThreadPromptRoutingRequiresRegistryAndPromptCapableProvider() {
val promptProvider = TestAgentSessionProviderDescriptor(
@@ -1233,3 +1354,23 @@ private fun editorContext(): AgentChatEditorTabActionContext {
tabKey = "codex:$normalizedPath:thread-1",
)
}
private fun invocationData(): AgentPromptInvocationData {
val project = ProjectManager.getInstance().defaultProject
return AgentPromptInvocationData(
project = project,
actionId = AgentWorkbenchActionIds.Sessions.MainToolbar.NEW_THREAD,
actionText = "New Thread",
actionPlace = ActionPlaces.MAIN_TOOLBAR,
invokedAtMs = 0,
)
}
private fun contextItem(body: String): AgentPromptContextItem {
return AgentPromptContextItem(
rendererId = AgentPromptContextRendererIds.SNIPPET,
title = "Selection",
body = body,
source = "test",
)
}
@@ -10,7 +10,6 @@ import com.intellij.agent.workbench.sessions.buildAgentSessionLaunchProfileMenuM
import com.intellij.agent.workbench.sessions.launchQuickStartProfile
import com.intellij.agent.workbench.sessions.providerItemIconWithMode
import com.intellij.agent.workbench.sessions.resolveAgentSessionLaunchProfileSelection
import com.intellij.agent.workbench.sessions.service.AgentPreparedNewSessionLaunchContext
import com.intellij.agent.workbench.sessions.service.AgentSessionLaunchService
import com.intellij.agent.workbench.sessions.state.AgentSessionUiPreferencesStateService
import com.intellij.agent.workbench.sessions.statistics.AgentWorkbenchEntryPoint
@@ -61,7 +60,8 @@ internal class AgentSessionsTreePopupTaskFolderAgentGroup @JvmOverloads construc
e.presentation.isPerformGroup = enabled
e.presentation.icon = quickStartItem?.let { providerItemIconWithMode(it.menuItem) }
if (!enabled) {
e.presentation.description = AgentSessionsBundle.message("action.AgentWorkbenchSessions.TreePopup.TaskFolderAgent.disabled.description")
e.presentation.description =
AgentSessionsBundle.message("action.AgentWorkbenchSessions.TreePopup.TaskFolderAgent.disabled.description")
}
}
@@ -145,8 +145,8 @@ internal fun createTaskFolderAgentViaService(
launchProfileId = profile.id,
entryPoint = entryPoint,
currentProject = project,
initialMessageRequestBuilder = { context ->
AgentPromptInitialMessageRequest(prompt = buildTaskFolderAgentPrompt(path = path, folder = folder, context = context))
initialMessageRequestBuilder = {
AgentPromptInitialMessageRequest(prompt = buildTaskFolderAgentPrompt(path = path, folder = folder))
},
preparedLaunchHandler = folder?.let { taskFolder ->
{ context ->
@@ -178,10 +178,9 @@ private fun resolveTaskFolderAgentLaunchProfileSelection(
)
}
private fun buildTaskFolderAgentPrompt(
internal fun buildTaskFolderAgentPrompt(
path: String,
folder: AgentTaskFolder?,
context: AgentPreparedNewSessionLaunchContext,
): String {
if (folder != null) {
return """
@@ -189,8 +188,6 @@ private fun buildTaskFolderAgentPrompt(
Project path: $path
Task folder name: ${folder.name}
Task folder id: ${folder.id}
Current thread id: ${context.threadId}
The IDE has already assigned this thread to the task folder. If the work needs issue tracker association, use task folder metadata key "issue".
Ask for any missing task details, then proceed with the task.
@@ -201,7 +198,6 @@ private fun buildTaskFolderAgentPrompt(
Start an Agent Workbench task-folder workflow for this project.
Project path: $path
Current thread id: ${context.threadId}
When you know the task folder name, call the agent_workbench_create_task_folder tool. If the user mentions an issue tracker id, pass it as the issue parameter.
If the task details are missing, ask one concise follow-up before creating the folder.
@@ -33,6 +33,7 @@ import com.intellij.agent.workbench.sessions.toolwindow.actions.AgentSessionsTre
import com.intellij.agent.workbench.sessions.toolwindow.actions.AgentSessionsTreePopupUnarchiveThreadAction
import com.intellij.agent.workbench.sessions.toolwindow.actions.CreateTaskFolderRequest
import com.intellij.agent.workbench.sessions.toolwindow.actions.TaskFolderMetadataEdit
import com.intellij.agent.workbench.sessions.toolwindow.actions.buildTaskFolderAgentPrompt
import com.intellij.agent.workbench.sessions.toolwindow.actions.canMoveThreadsToTaskFolder
import com.intellij.agent.workbench.sessions.toolwindow.actions.createAgentSessionsTreePopupActionContext
import com.intellij.agent.workbench.sessions.toolwindow.actions.resolveAgentSessionsTreePopupActionContext
@@ -583,6 +584,36 @@ class AgentSessionsTreePopupActionsTest {
assertThat(openedProject).isSameAs(context.project)
}
@Test
fun taskFolderAgentPromptForNewFolderOmitsInternalIds() {
val prompt = buildTaskFolderAgentPrompt(path = "/work/project-a", folder = null)
assertThat(prompt)
.contains(
"Start an Agent Workbench task-folder workflow for this project.",
"Project path: /work/project-a",
"agent_workbench_create_task_folder",
)
.doesNotContain("Current thread id", "Task folder id", "thread-1")
}
@Test
fun taskFolderAgentPromptForExistingFolderOmitsInternalIds() {
val prompt = buildTaskFolderAgentPrompt(
path = "/work/project-a",
folder = taskFolder(path = "/work/project-a", name = "Authentication rewrite"),
)
assertThat(prompt)
.contains(
"Continue this Agent Workbench task folder.",
"Project path: /work/project-a",
"Task folder name: Authentication rewrite",
"The IDE has already assigned this thread to the task folder.",
)
.doesNotContain("Current thread id", "Task folder id", "folder1", "thread-1")
}
@Test
fun setTaskFolderMetadataActionStoresIssuePreset() {
val appliedMetadata = mutableListOf<Triple<String, String, String>>()
@@ -51,11 +51,12 @@ Date: 2026-05-09
[@test] ../../prompt/ui/testSrc/AgentPromptContextPersistenceDecisionsTest.kt
- Context block soft cap is 12,000 characters. Exceeding it requires explicit send-full, auto-trim, or cancel before launch.
[@test] ../../prompt/ui/testSrc/AgentPromptContextSoftCapPolicyTest.kt
[@test] ../../prompt/core/testSrc/AgentPromptContextEnvelopeFormatterTest.kt
## Testing / Local Run
- `./tests.cmd --module intellij.agent.workbench.prompt.ui.tests --test "com.intellij.agent.workbench.prompt.ui.actions.*"`
- `./tests.cmd --module intellij.agent.workbench.prompt.ui.tests --test "com.intellij.agent.workbench.prompt.ui.AgentPromptContext*Test"`
- `./tests.cmd --module intellij.agent.workbench.prompt.core.tests --test com.intellij.agent.workbench.prompt.core.AgentPromptContextEnvelopeFormatterTest`
- `./tests.cmd --module intellij.agent.workbench.plugin.tests --test com.intellij.agent.workbench.plugin.AgentWorkbenchAddToAgentContextActionRegistrationTest`
- `./tests.cmd --module intellij.agent.workbench.chat.tests --test com.intellij.agent.workbench.chat.AgentChatOpenTopLevelDispatchTest`
@@ -79,6 +79,9 @@ New-thread actions let users start provider-backed threads from project/worktree
[@test] ../../prompt/ui/testSrc/AgentPromptPaletteSessionControllerTest.kt
[@test] ../../chat/testSrc/AgentChatFileEditorLifecycleTest.kt
- 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] ../../chat/testSrc/AgentChatFileEditorLifecycleTest.kt
@@ -34,14 +34,15 @@ Prompt context is collected by contributors, normalized by the prompt UI, and re
- Context normalization removes invalid/blank entries, preserves meaningful payload metadata, and applies truncation metadata rather than silently dropping source context.
[@test] ../../prompt/ui/testSrc/AgentPromptContextNormalizationDecisionsTest.kt
- Context soft cap is enforced before launch and requires explicit user choice when exceeded.
[@test] ../../prompt/ui/testSrc/AgentPromptContextSoftCapPolicyTest.kt
- Context soft cap preparation and auto-trim policy live in prompt core. UI launch paths require explicit user choice when the prepared context exceeds the soft cap.
[@test] ../../prompt/core/testSrc/AgentPromptContextEnvelopeFormatterTest.kt
- Prompt draft persistence must not serialize manual context; runtime-only session state may preserve context for the current IDE session.
[@test] ../../prompt/ui/testSrc/AgentPromptUiSessionStateServiceTest.kt
## Testing / Local Run
- `./tests.cmd --module intellij.agent.workbench.prompt.core.tests --test com.intellij.agent.workbench.prompt.core.AgentPromptContextResolverServiceTest`
- `./tests.cmd --module intellij.agent.workbench.prompt.core.tests --test com.intellij.agent.workbench.prompt.core.AgentPromptContextEnvelopeFormatterTest`
- `./tests.cmd --module intellij.agent.workbench.prompt.ui.tests --test "com.intellij.agent.workbench.prompt.ui.AgentPromptContext*Test"`
- `./tests.cmd --module intellij.agent.workbench.prompt.ui.tests --test com.intellij.agent.workbench.prompt.ui.AgentPromptUiSessionStateServiceTest`
@@ -96,6 +96,7 @@ Agent Workbench treats Pi as a first-class terminal-backed provider. Pi sessions
for explicit folder mutations, must not inject prompt context automatically, and must return normal `response` frames with `ok`,
`requestId`, and the requested folder, assignment, metadata, or mutation result fields.
[@test] ../../lib-agent/providers/pi/sessions/testSrc/PiExtensionControlWebSocketHandlerTest.kt
[@test] ../../lib-agent/providers/pi/sessions/testSrc/PiThemeSupportTest.kt
- Rename must append a Pi-compatible `session_info` entry to the session JSONL file while preserving the thread's current archive state. Archive and unarchive must use the same mechanism by writing a title with or without the shared `[archived] ` prefix; loaded Pi titles must strip that prefix for display and use it only as Agent Workbench archive state.
[@test] ../../lib-agent/providers/pi/sessions/testSrc/PiSessionSourceTest.kt