mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-10-06 06:21:16 +07:00
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:
committed by
intellij-monorepo-bot
parent
bc2e559138
commit
1e97f8d180
@@ -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()) {
|
||||
|
||||
+126
@@ -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",
|
||||
)
|
||||
}
|
||||
}
|
||||
+65
-11
@@ -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,
|
||||
|
||||
+141
@@ -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",
|
||||
)
|
||||
}
|
||||
|
||||
+5
-9
@@ -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.
|
||||
|
||||
+31
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user