diff --git a/plugins/agent-workbench/codex/common/src/CodexAppServerClient.kt b/plugins/agent-workbench/codex/common/src/CodexAppServerClient.kt index 57134353d90b..7ee8cc0838cb 100644 --- a/plugins/agent-workbench/codex/common/src/CodexAppServerClient.kt +++ b/plugins/agent-workbench/codex/common/src/CodexAppServerClient.kt @@ -283,7 +283,7 @@ class CodexAppServerClient( val configuredExecutable = executablePathProvider() ?.trim() ?.takeIf { it.isNotEmpty() } - val executable = configuredExecutable ?: CODEX_COMMAND + val executable = configuredExecutable ?: CodexCliUtils.CODEX_COMMAND val process = try { GeneralCommandLine(executable, "app-server") .withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.CONSOLE) @@ -295,7 +295,6 @@ class CodexAppServerClient( } } .createProcess() - .createProcess() } catch (t: Throwable) { if (configuredExecutable == null && isExecutableNotFound(t)) { diff --git a/plugins/agent-workbench/sessions/BUILD.bazel b/plugins/agent-workbench/sessions/BUILD.bazel index 49d2906b00b6..0c8e571bf42e 100644 --- a/plugins/agent-workbench/sessions/BUILD.bazel +++ b/plugins/agent-workbench/sessions/BUILD.bazel @@ -82,4 +82,4 @@ jps_test( name = "sessions_test", runtime_deps = [":sessions_test_lib"] ) -### auto-generated section `test intellij.agent.workbench.sessions` end +### auto-generated section `test intellij.agent.workbench.sessions` end \ No newline at end of file diff --git a/plugins/agent-workbench/sessions/src/AgentSessionsService.kt b/plugins/agent-workbench/sessions/src/AgentSessionsService.kt index d2bb66d6ea88..d0271ada3c15 100644 --- a/plugins/agent-workbench/sessions/src/AgentSessionsService.kt +++ b/plugins/agent-workbench/sessions/src/AgentSessionsService.kt @@ -41,7 +41,6 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import java.nio.file.InvalidPathException import java.nio.file.Path -import java.util.UUID import kotlin.io.path.invariantSeparatorsPathString import kotlin.io.path.name @@ -49,7 +48,7 @@ private val LOG = logger() private const val SUPPRESS_BRANCH_MISMATCH_DIALOG_KEY = "agent.workbench.suppress.branch.mismatch.dialog" private const val OPEN_PROJECT_ACTION_KEY_PREFIX = "project-open" -private const val CREATE_THREAD_ACTION_KEY_PREFIX = "thread-create" +private const val CREATE_SESSION_ACTION_KEY_PREFIX = "session-create" private const val OPEN_THREAD_ACTION_KEY_PREFIX = "thread-open" private const val OPEN_SUB_AGENT_ACTION_KEY_PREFIX = "subagent-open" @@ -337,26 +336,6 @@ internal class AgentSessionsService private constructor( } } - fun createAndOpenThread(path: String, currentProject: Project? = null) { - val normalized = normalizePath(path) - val key = buildCreateThreadActionKey(normalized) - actionGate.launch( - scope = serviceScope, - key = key, - policy = SingleFlightPolicy.DROP, - progress = dedicatedFrameOpenProgressRequest(currentProject), - onDrop = { LOG.debug("Dropped duplicate create thread action for $normalized") }, - ) { - val freshThread = createFreshCodexThread() - openChat( - path = normalized, - thread = freshThread, - subAgent = null, - shellCommandOverride = buildAgentSessionNewCommand(AgentSessionProvider.CODEX), - ) - } - } - fun showMoreProjects() { mutableState.update { it.copy(visibleProjectCount = it.visibleProjectCount + DEFAULT_VISIBLE_PROJECT_COUNT) } } @@ -430,9 +409,23 @@ internal class AgentSessionsService private constructor( } } - fun createNewSession(path: String, provider: AgentSessionProvider, yolo: Boolean = false) { - serviceScope.launch { - val normalized = normalizePath(path) + fun createNewSession( + path: String, + provider: AgentSessionProvider, + yolo: Boolean = false, + currentProject: Project? = null, + ) { + val normalized = normalizePath(path) + val key = buildCreateSessionActionKey(normalized, provider, yolo) + actionGate.launch( + scope = serviceScope, + key = key, + policy = SingleFlightPolicy.DROP, + progress = dedicatedFrameOpenProgressRequest(currentProject), + onDrop = { + LOG.debug("Dropped duplicate create session action for $normalized:$provider:yolo=$yolo") + }, + ) { service().setLastUsedProvider(provider) val identity: String @@ -639,8 +632,8 @@ internal class AgentSessionsService private constructor( return "$OPEN_PROJECT_ACTION_KEY_PREFIX:$path" } - private fun buildCreateThreadActionKey(path: String): String { - return "$CREATE_THREAD_ACTION_KEY_PREFIX:$path" + private fun buildCreateSessionActionKey(path: String, provider: AgentSessionProvider, yolo: Boolean): String { + return "$CREATE_SESSION_ACTION_KEY_PREFIX:$path:$provider:yolo=$yolo" } private fun buildOpenThreadActionKey(path: String, thread: AgentSessionThread): String { @@ -1126,17 +1119,6 @@ internal class AgentSessionsService private constructor( return null } - private fun createFreshCodexThread(): AgentSessionThread { - return AgentSessionThread( - id = "new-${UUID.randomUUID()}", - title = AgentSessionsBundle.message("toolwindow.provider.codex"), - updatedAt = System.currentTimeMillis(), - archived = false, - activity = AgentSessionActivity.PROCESSING, - provider = AgentSessionProvider.CODEX, - ) - } - internal data class ProjectEntry( val path: String, val name: String, diff --git a/plugins/agent-workbench/sessions/src/AgentSessionsToolWindow.kt b/plugins/agent-workbench/sessions/src/AgentSessionsToolWindow.kt index d6c34be32321..0ebc65ed6de1 100644 --- a/plugins/agent-workbench/sessions/src/AgentSessionsToolWindow.kt +++ b/plugins/agent-workbench/sessions/src/AgentSessionsToolWindow.kt @@ -50,15 +50,14 @@ internal fun agentSessionsToolWindow(currentProject: Project) { state = state, onRefresh = { service.refresh() }, onOpenProject = { service.openOrFocusProject(it) }, - onCreateThread = { path -> service.createAndOpenThread(path, currentProject) }, onProjectExpanded = { service.loadProjectThreadsOnDemand(it) }, onWorktreeExpanded = { projectPath, worktreePath -> service.loadWorktreeThreadsOnDemand(projectPath, worktreePath) }, - onOpenThread = { path, thread -> service.openChatThread(path, thread, currentProject) }, - onOpenSubAgent = { path, thread, subAgent -> service.openChatSubAgent(path, thread, subAgent, currentProject) }, - onCreateSession = { path, provider, yolo -> service.createNewSession(path, provider, yolo) }, - lastUsedProvider = lastUsedProvider, + onOpenThread = { path, thread -> service.openChatThread(path, thread, currentProject) }, + onOpenSubAgent = { path, thread, subAgent -> service.openChatSubAgent(path, thread, subAgent, currentProject) }, + onCreateSession = { path, provider, yolo -> service.createNewSession(path, provider, yolo, currentProject) }, + lastUsedProvider = lastUsedProvider, visibleProjectCount = state.visibleProjectCount, onShowMoreProjects = { service.showMoreProjects() }, visibleThreadCounts = state.visibleThreadCounts, @@ -72,7 +71,6 @@ internal fun agentSessionsToolWindowContent( state: AgentSessionsState, onRefresh: () -> Unit, onOpenProject: (String) -> Unit, - onCreateThread: (String) -> Unit = {}, onProjectExpanded: (String) -> Unit = {}, onWorktreeExpanded: (String, String) -> Unit = { _, _ -> }, onOpenThread: (String, AgentSessionThread) -> Unit = { _, _ -> }, @@ -98,7 +96,6 @@ internal fun agentSessionsToolWindowContent( projects = state.projects, onRefresh = onRefresh, onOpenProject = onOpenProject, - onCreateThread = onCreateThread, onProjectExpanded = onProjectExpanded, onWorktreeExpanded = onWorktreeExpanded, onOpenThread = onOpenThread, diff --git a/plugins/agent-workbench/sessions/src/SessionTree.kt b/plugins/agent-workbench/sessions/src/SessionTree.kt index 6cea716a7e9e..ad7a72206191 100644 --- a/plugins/agent-workbench/sessions/src/SessionTree.kt +++ b/plugins/agent-workbench/sessions/src/SessionTree.kt @@ -4,10 +4,7 @@ import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import com.intellij.agent.workbench.chat.AgentChatTabSelection import org.jetbrains.jewel.foundation.ExperimentalJewelApi @@ -21,15 +18,12 @@ import org.jetbrains.jewel.ui.component.styling.LazyTreeMetrics import org.jetbrains.jewel.ui.component.styling.LazyTreeStyle import org.jetbrains.jewel.ui.theme.treeStyle -private const val PROJECT_CLICK_SUPPRESSION_MS = 500L - @OptIn(ExperimentalJewelApi::class) @Composable internal fun sessionTree( projects: List, onRefresh: () -> Unit, onOpenProject: (String) -> Unit, - onCreateThread: (String) -> Unit, onProjectExpanded: (String) -> Unit, onWorktreeExpanded: (String, String) -> Unit = { _, _ -> }, onOpenThread: (String, AgentSessionThread) -> Unit, @@ -78,27 +72,6 @@ internal fun sessionTree( val tree = remember(projects, visibleProjectCount, visibleThreadCounts) { buildSessionTree(projects, visibleProjectCount, visibleThreadCounts) } - var suppressedProjectClick by remember { mutableStateOf(null) } - - fun suppressNextProjectClick(path: String) { - suppressedProjectClick = SuppressedProjectClick( - path = path, - expiresAt = System.currentTimeMillis() + PROJECT_CLICK_SUPPRESSION_MS, - ) - } - - fun shouldSuppressProjectClick(path: String): Boolean { - val suppression = suppressedProjectClick ?: return false - if (System.currentTimeMillis() > suppression.expiresAt) { - suppressedProjectClick = null - return false - } - if (suppression.path != path) { - return false - } - suppressedProjectClick = null - return true - } val treeStyle = run { val baseStyle = JewelTheme.treeStyle @@ -127,9 +100,7 @@ internal fun sessionTree( onElementClick = { element -> when (val node = element.data) { is SessionTreeNode.Project -> { - if (!shouldSuppressProjectClick(node.project.path)) { - onOpenProject(node.project.path) - } + onOpenProject(node.project.path) } is SessionTreeNode.Thread -> { val path = when (val id = element.id) { @@ -166,10 +137,6 @@ internal fun sessionTree( sessionTreeNodeContent( element = element, onOpenProject = onOpenProject, - onCreateThread = { path -> - suppressNextProjectClick(path) - onCreateThread(path) - }, onRefresh = onRefresh, onCreateSession = onCreateSession, lastUsedProvider = lastUsedProvider, @@ -179,11 +146,6 @@ internal fun sessionTree( } } -private data class SuppressedProjectClick( - val path: String, - val expiresAt: Long, -) - private fun buildSessionTree( projects: List, visibleProjectCount: Int, diff --git a/plugins/agent-workbench/sessions/src/SessionTreeRows.kt b/plugins/agent-workbench/sessions/src/SessionTreeRows.kt index 0463eb3b8c60..155a2d2b143d 100644 --- a/plugins/agent-workbench/sessions/src/SessionTreeRows.kt +++ b/plugins/agent-workbench/sessions/src/SessionTreeRows.kt @@ -3,7 +3,6 @@ package com.intellij.agent.workbench.sessions import androidx.compose.foundation.ContextMenuArea import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsHoveredAsState @@ -25,8 +24,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.takeOrElse -import androidx.compose.ui.input.pointer.PointerIcon -import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp @@ -37,20 +34,17 @@ import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.foundation.theme.LocalContentColor import org.jetbrains.jewel.ui.component.CircularProgressIndicator import org.jetbrains.jewel.ui.component.ContextMenuItemOption -import org.jetbrains.jewel.ui.component.Icon import org.jetbrains.jewel.ui.component.OutlinedButton import org.jetbrains.jewel.ui.component.Text import org.jetbrains.jewel.ui.component.Tooltip import org.jetbrains.jewel.ui.component.search.highlightSpeedSearchMatches import org.jetbrains.jewel.ui.component.search.highlightTextSearch -import org.jetbrains.jewel.ui.icons.AllIconsKeys @OptIn(ExperimentalJewelApi::class) @Composable internal fun SelectableLazyItemScope.sessionTreeNodeContent( element: Tree.Element, onOpenProject: (String) -> Unit, - onCreateThread: (String) -> Unit, onRefresh: () -> Unit, onCreateSession: (String, AgentSessionProvider, Boolean) -> Unit = { _, _, _ -> }, lastUsedProvider: AgentSessionProvider? = null, @@ -61,7 +55,6 @@ internal fun SelectableLazyItemScope.sessionTreeNodeContent( is SessionTreeNode.Project -> projectNodeRow( project = node.project, onOpenProject = onOpenProject, - onCreateThread = onCreateThread, onCreateSession = onCreateSession, lastUsedProvider = lastUsedProvider, ) @@ -138,8 +131,6 @@ private fun rememberTreeRowChrome( private fun SelectableLazyItemScope.projectNodeRow( project: AgentProjectSessions, onOpenProject: (String) -> Unit, - onCreateThread: (String) -> Unit, - onCreateThread: (String) -> Unit, onCreateSession: (String, AgentSessionProvider, Boolean) -> Unit, lastUsedProvider: AgentSessionProvider?, ) { @@ -203,14 +194,7 @@ private fun SelectableLazyItemScope.projectNodeRow( NewSessionHoverActions( path = project.path, lastUsedProvider = lastUsedProvider, - onCreateSession = { path, provider, yolo -> - if (provider == AgentSessionProvider.CODEX && !yolo && lastUsedProvider == null) { - onCreateThread(path) - } - else { - onCreateSession(path, provider, yolo) - } - }, + onCreateSession = onCreateSession, popupVisible = newSessionPopupVisible, onPopupVisibleChange = { newSessionPopupVisible = it }, ) diff --git a/plugins/agent-workbench/sessions/src/providers/codex/CodexAppServerSessionBackend.kt b/plugins/agent-workbench/sessions/src/providers/codex/CodexAppServerSessionBackend.kt deleted file mode 100644 index 9b5c8cc41282..000000000000 --- a/plugins/agent-workbench/sessions/src/providers/codex/CodexAppServerSessionBackend.kt +++ /dev/null @@ -1,41 +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.sessions.providers.codex - -import com.intellij.agent.workbench.codex.common.CodexAppServerClient -import com.intellij.agent.workbench.codex.common.CodexAppServerException -import com.intellij.agent.workbench.sessions.codex.CodexProjectSessionService -import com.intellij.agent.workbench.sessions.codex.resolveProjectDirectoryFromPath -import com.intellij.openapi.project.Project -import kotlinx.coroutines.CoroutineScope - -@Suppress("unused") -internal class CodexAppServerSessionBackend( - private val coroutineScope: CoroutineScope, -) : CodexSessionBackend { - override suspend fun listThreads(path: String, openProject: Project?): List { - if (openProject != null) { - return listThreadsFromOpenProject(openProject) - } - return listThreadsFromClosedPath(path) - } - - private suspend fun listThreadsFromOpenProject(project: Project): List { - val service = project.getService(CodexProjectSessionService::class.java) - if (service == null || !service.hasWorkingDirectory()) { - throw CodexAppServerException("Project directory is not available") - } - return service.listThreads().map(::CodexBackendThread) - } - - private suspend fun listThreadsFromClosedPath(path: String): List { - val workingDirectory = resolveProjectDirectoryFromPath(path) - ?: return emptyList() - val client = CodexAppServerClient(coroutineScope = coroutineScope, workingDirectory = workingDirectory) - try { - return client.listThreads(archived = false).map(::CodexBackendThread) - } - finally { - client.shutdown() - } - } -} diff --git a/plugins/agent-workbench/sessions/testSrc/AgentSessionsToolWindowTest.kt b/plugins/agent-workbench/sessions/testSrc/AgentSessionsToolWindowTest.kt index 8fdc531c8682..f8140a17cf3a 100644 --- a/plugins/agent-workbench/sessions/testSrc/AgentSessionsToolWindowTest.kt +++ b/plugins/agent-workbench/sessions/testSrc/AgentSessionsToolWindowTest.kt @@ -100,8 +100,10 @@ class AgentSessionsToolWindowTest { } @Test - fun hoveringProjectRowShowsNewThreadActionAndDoesNotInvokeOpenCallback() { - var createdPath: String? = null + fun hoveringProjectRowShowsQuickCreateSessionActionAndDoesNotInvokeOpenCallback() { + var createdSessionPath: String? = null + var createdSessionProvider: AgentSessionProvider? = null + var createdSessionYolo: Boolean? = null var openedPath: String? = null val projectPath = "/work/project-plus" val projects = listOf( @@ -117,23 +119,30 @@ class AgentSessionsToolWindowTest { state = AgentSessionsState(projects = projects), onRefresh = {}, onOpenProject = { openedPath = it }, - onCreateThread = { createdPath = it }, + onCreateSession = { path, provider, yolo -> + createdSessionPath = path + createdSessionProvider = provider + createdSessionYolo = yolo + }, + lastUsedProvider = AgentSessionProvider.CLAUDE, ) } - val newThreadLabel = AgentSessionsBundle.message("toolwindow.action.new.thread") - composeRule.onAllNodesWithContentDescription(newThreadLabel).assertCountEquals(0) + val quickActionLabel = AgentSessionsBundle.message("toolwindow.provider.claude") + composeRule.onAllNodesWithContentDescription(quickActionLabel).assertCountEquals(0) composeRule.onNodeWithText("Project Plus") .assertIsDisplayed() .performMouseInput { moveTo(center) } - composeRule.onNodeWithContentDescription(newThreadLabel) + composeRule.onNodeWithContentDescription(quickActionLabel) .assertIsDisplayed() .performClick() composeRule.runOnIdle { - assertThat(createdPath).isEqualTo(projectPath) + assertThat(createdSessionPath).isEqualTo(projectPath) + assertThat(createdSessionProvider).isEqualTo(AgentSessionProvider.CLAUDE) + assertThat(createdSessionYolo).isFalse() assertThat(openedPath).isNull() } } diff --git a/plugins/agent-workbench/sessions/testSrc/CodexAppServerClientTestSupport.kt b/plugins/agent-workbench/sessions/testSrc/CodexAppServerClientTestSupport.kt index 6c36e756a018..e96761daa47e 100644 --- a/plugins/agent-workbench/sessions/testSrc/CodexAppServerClientTestSupport.kt +++ b/plugins/agent-workbench/sessions/testSrc/CodexAppServerClientTestSupport.kt @@ -215,10 +215,21 @@ private fun resolveTestClasspath(): String { .map(String::trim) .filter(String::isNotEmpty) if (entries.isNotEmpty()) { - return entries.joinToString(File.pathSeparator) + return absolutizeClasspathEntries(entries) } } - return System.getProperty("java.class.path") + val classpath = System.getProperty("java.class.path") + val entries = classpath.split(File.pathSeparator) + .map(String::trim) + .filter(String::isNotEmpty) + return absolutizeClasspathEntries(entries) +} + +private fun absolutizeClasspathEntries(entries: List): String { + return entries.joinToString(File.pathSeparator) { entry -> + val path = Path.of(entry) + if (path.isAbsolute) entry else path.toAbsolutePath().normalize().toString() + } } private fun writeAppServerArgsFile(tempDir: Path, classpath: String, configPath: Path): Path { diff --git a/plugins/agent-workbench/sessions/testSrc/ComposeTestUtils.kt b/plugins/agent-workbench/sessions/testSrc/ComposeTestUtils.kt index 299450051437..8797e81a4630 100644 --- a/plugins/agent-workbench/sessions/testSrc/ComposeTestUtils.kt +++ b/plugins/agent-workbench/sessions/testSrc/ComposeTestUtils.kt @@ -20,11 +20,17 @@ import org.jetbrains.jewel.foundation.theme.ThemeDefinition import org.jetbrains.jewel.foundation.theme.ThemeIconData import org.jetbrains.jewel.intui.standalone.theme.default import org.jetbrains.jewel.ui.ComponentStyling +import org.jetbrains.jewel.ui.LocalMenuItemShortcutHintProvider +import org.jetbrains.jewel.ui.LocalMenuItemShortcutProvider import org.jetbrains.jewel.ui.LocalTypography +import org.jetbrains.jewel.ui.MenuItemShortcutHintProvider +import org.jetbrains.jewel.ui.MenuItemShortcutProvider import org.jetbrains.jewel.ui.Typography +import org.jetbrains.jewel.ui.component.ContextMenuItemOptionAction import org.jetbrains.jewel.ui.icon.LocalNewUiChecker import org.jetbrains.jewel.ui.icon.NewUiChecker import org.jetbrains.jewel.ui.theme.BaseJewelTheme +import javax.swing.KeyStroke internal fun ComposeContentTestRule.setContentWithTheme(content: @Composable () -> Unit) { setContent { @@ -32,6 +38,8 @@ internal fun ComposeContentTestRule.setContentWithTheme(content: @Composable () CompositionLocalProvider( LocalTypography provides TestTypography, LocalNewUiChecker provides TestNewUiChecker, + LocalMenuItemShortcutProvider provides EmptyMenuItemShortcutProvider, + LocalMenuItemShortcutHintProvider provides EmptyMenuItemShortcutHintProvider, ) { content() } @@ -131,3 +139,11 @@ private object TestTypography : Typography { private object TestNewUiChecker : NewUiChecker { override fun isNewUi(): Boolean = true } + +private object EmptyMenuItemShortcutProvider : MenuItemShortcutProvider { + override fun getShortcutKeyStroke(actionType: ContextMenuItemOptionAction): KeyStroke? = null +} + +private object EmptyMenuItemShortcutHintProvider : MenuItemShortcutHintProvider { + override fun getShortcutHint(actionType: ContextMenuItemOptionAction): String = "" +} diff --git a/plugins/agent-workbench/spec/actions/new-thread.spec.md b/plugins/agent-workbench/spec/actions/new-thread.spec.md new file mode 100644 index 000000000000..5a65a240840a --- /dev/null +++ b/plugins/agent-workbench/spec/actions/new-thread.spec.md @@ -0,0 +1,84 @@ +--- +name: Agent Sessions New-Session Actions +description: Requirements for project/worktree new-session actions and provider-specific new-session routing in Agent Threads. +targets: + - ../../sessions/src/SessionTreeNewSessionActions.kt + - ../../sessions/src/SessionTreeRows.kt + - ../../sessions/src/AgentSessionsToolWindow.kt + - ../../sessions/src/AgentSessionsService.kt + - ../../sessions/src/codex/SharedCodexAppServerService.kt + - ../../sessions/src/AgentSessionCli.kt + - ../../sessions/src/providers/codex/CodexCliCommands.kt + - ../../sessions/resources/messages/AgentSessionsBundle.properties + - ../../sessions/testSrc/AgentSessionsToolWindowTest.kt + - ../../sessions/testSrc/AgentSessionCliTest.kt + - ../../sessions/testSrc/CodexAppServerClientTest.kt +--- + +# Agent Sessions New-Session Actions + +Status: Draft +Date: 2026-02-13 + +## Summary +Define project/worktree `New Session` actions (`+` and quick-provider icon) and map each provider + mode to the chat-open flow. + +## Goals +- Keep session-creation behavior identical across project and worktree rows. +- Keep provider/mode semantics explicit and testable. +- Avoid reintroducing legacy Codex "fresh session without thread id" flow. + +## Non-goals +- Thread loading/sorting/paging behavior. +- Dedicated-frame routing rules outside new-session open progress integration. +- New user-facing warning text for `Codex (Full Auto)`. + +## Requirements +- Project/worktree row hover actions must route through `onCreateSession(path, provider, yolo)`; no separate create-thread callback path may be used. + [@test] ../../sessions/testSrc/AgentSessionsToolWindowTest.kt +- Quick provider icon (when `lastUsedProvider` exists) must create a session with `yolo=false`. +- Popup must expose four selectable entries: Claude, Codex, Claude YOLO, Codex YOLO. +- Popup section label remains `YOLO`; Codex YOLO entry label remains `Codex (Full Auto)`. + +- `AgentSessionsService.createNewSession(path, provider, yolo, currentProject)` is the service entry point for row actions. +- `createNewSession` must deduplicate in-flight actions by normalized `path + provider + yolo` with single-flight `DROP`. +- `createNewSession` must set `lastUsedProvider` to the selected provider before opening chat. + +- Claude `yolo=false` new-session command must be `claude`. +- Claude `yolo=true` new-session command must be `claude --dangerously-skip-permissions`. +- Codex new-session must not use direct `codex`/`--full-auto` command construction for session creation. + [@test] ../../sessions/testSrc/AgentSessionCliTest.kt + +- Codex new-session must call app-server `thread/start`, persist the new thread id, then open chat with `codex resume `. +- Codex `yolo=true` must call `thread/start` with `approvalPolicy="on-request"` and `sandbox="workspace-write"`. +- Codex `yolo=false` must call `thread/start` with default app-server parameters (no forced approval/sandbox overrides). + [@test] ../../sessions/testSrc/CodexAppServerClientTest.kt + +- `Codex (Full Auto)` semantics are documented in spec and do not require extra warning text in UI. + +## User Experience +- Hovering a project/worktree row reveals new-session controls without opening the project. +- The quick-provider icon provides one-click repeat creation. +- Popup grouping keeps normal and YOLO choices explicit. + +## Data & Backend +- Codex action flow uses `SharedCodexAppServerService.createThread(cwd, yolo)` and persists created threads before resume. +- Codex new-session identity is concrete (`CODEX:`), not synthetic `new-*`. + +## Error Handling +- Provider CLI/app-server failures must continue using provider-specific error paths in existing service flow. +- Duplicate clicks on the same action tuple must be dropped rather than opening multiple sessions. + +## Testing / Local Run +- `./tests.cmd '-Dintellij.build.test.patterns=com.intellij.agent.workbench.sessions.AgentSessionsToolWindowTest'` +- `./tests.cmd '-Dintellij.build.test.patterns=com.intellij.agent.workbench.sessions.AgentSessionCliTest'` +- `./tests.cmd '-Dintellij.build.test.patterns=com.intellij.agent.workbench.sessions.CodexAppServerClientTest'` + +## Open Questions / Risks +- `createNewSession` dedup + yolo parameter mapping currently has limited direct service-level test coverage. + +## References +- `../agent-sessions.spec.md` +- `../agent-sessions-codex-rollout-source.spec.md` +- `../agent-dedicated-frame.spec.md` + diff --git a/plugins/agent-workbench/spec/agent-dedicated-frame.spec.md b/plugins/agent-workbench/spec/agent-dedicated-frame.spec.md index 58e81508148c..6787d64cdec3 100644 --- a/plugins/agent-workbench/spec/agent-dedicated-frame.spec.md +++ b/plugins/agent-workbench/spec/agent-dedicated-frame.spec.md @@ -54,7 +54,7 @@ Define dedicated-frame behavior for Agent chat opening. By default, chat opens i - Chat resume command remains provider-specific in dedicated mode as in current-project mode: - Codex: `codex resume ` - Claude: `claude --resume ` -- Codex fresh-thread opens (project-row `New Thread`) use `codex` without `resume`. +- New-session action semantics (including Codex `Codex (Full Auto)` mapping) are defined in `spec/actions/new-thread.spec.md` and do not change between dedicated/current-project modes. [@test] ../sessions/testSrc/AgentSessionsGearActionsTest.kt [@test] ../sessions/testSrc/AgentSessionsOpenModeRoutingTest.kt @@ -94,4 +94,5 @@ Define dedicated-frame behavior for Agent chat opening. By default, chat opens i ## References - `spec/agent-chat-editor.spec.md` - `spec/agent-sessions.spec.md` +- `spec/actions/new-thread.spec.md` - `community/platform/platform-impl/src/com/intellij/openapi/wm/ex/WelcomeScreenProjectProvider.kt` diff --git a/plugins/agent-workbench/spec/agent-sessions-codex-rollout-source.spec.md b/plugins/agent-workbench/spec/agent-sessions-codex-rollout-source.spec.md index 60946449dc43..9234b6d91b15 100644 --- a/plugins/agent-workbench/spec/agent-sessions-codex-rollout-source.spec.md +++ b/plugins/agent-workbench/spec/agent-sessions-codex-rollout-source.spec.md @@ -21,7 +21,7 @@ Codex thread discovery for Agent Threads defaults to rollout files under `~/.cod - Make Codex thread indicators reflect real activity based on rollout data. - Keep app-server implementation in code as an alternate backend. - Avoid archived-session handling in this iteration. -- Fix Codex `+` action to open fresh CLI chat (`codex`) without pre-creating a session. +- Keep rollout backend changes independent from new-session action semantics. ## Non-goals - Archived session browsing or unarchive actions. @@ -41,7 +41,7 @@ Codex thread discovery for Agent Threads defaults to rollout files under `~/.cod - `reviewing`: teal (`#2FD1C4`) - `processing`: orange (`#FF9F43`) - `ready`: green (`#3FE47E`) -- Codex project-row `+` action must open a fresh chat command `codex` (no `codex resume `). +- New-session action semantics (including Codex `Codex (Full Auto)` parameters) are defined in `spec/actions/new-thread.spec.md` and are backend-invariant. - Existing thread open behavior remains `codex resume `. ## Data & Backend @@ -57,3 +57,4 @@ Codex thread discovery for Agent Threads defaults to rollout files under `~/.cod ## References - `spec/agent-sessions.spec.md` - `spec/agent-chat-editor.spec.md` +- `spec/actions/new-thread.spec.md` diff --git a/plugins/agent-workbench/spec/agent-sessions.spec.md b/plugins/agent-workbench/spec/agent-sessions.spec.md index 9f6be17ccaff..6de3ca5ec287 100644 --- a/plugins/agent-workbench/spec/agent-sessions.spec.md +++ b/plugins/agent-workbench/spec/agent-sessions.spec.md @@ -14,7 +14,7 @@ targets: # Agent Threads Tool Window Status: Draft -Date: 2026-02-11 +Date: 2026-02-13 ## Summary Define the Agent Threads tool window as a provider-agnostic, project-scoped session browser. Threads from supported providers are aggregated per project/worktree, rendered in one tree, and opened through a shared chat routing flow. @@ -46,7 +46,7 @@ Define the Agent Threads tool window as a provider-agnostic, project-scoped sess - Resume command must be provider-specific: - Codex: `codex resume ` - Claude: `claude --resume ` -- Codex project-row `New Thread` action must open fresh CLI chat with `codex` (without pre-creating/resuming a session id). +- New-session action behavior (provider options, Codex/Claude command mapping, and Full Auto semantics) is defined in `spec/actions/new-thread.spec.md` and must be used by both project and worktree rows. - Branch mismatch between thread origin and current worktree branch must show a warning confirmation before opening chat. [@test] ../sessions/testSrc/AgentSessionLoadAggregationTest.kt @@ -86,3 +86,4 @@ Define the Agent Threads tool window as a provider-agnostic, project-scoped sess - `spec/agent-sessions-thread-visibility.spec.md` - `spec/agent-dedicated-frame.spec.md` - `spec/agent-chat-editor.spec.md` +- `spec/actions/new-thread.spec.md`