diff --git a/plugins/agent-workbench/claude/common/resources/intellij.agent.workbench.claude.common.xml b/plugins/agent-workbench/claude/common/resources/intellij.agent.workbench.claude.common.xml index e9f4a7dec93a..36f681611254 100644 --- a/plugins/agent-workbench/claude/common/resources/intellij.agent.workbench.claude.common.xml +++ b/plugins/agent-workbench/claude/common/resources/intellij.agent.workbench.claude.common.xml @@ -1 +1,7 @@ - + + + + + + + \ No newline at end of file diff --git a/plugins/agent-workbench/codex/sessions/resources/intellij.agent.workbench.codex.sessions.xml b/plugins/agent-workbench/codex/sessions/resources/intellij.agent.workbench.codex.sessions.xml index 19b56a701603..6bd8b4c35969 100644 --- a/plugins/agent-workbench/codex/sessions/resources/intellij.agent.workbench.codex.sessions.xml +++ b/plugins/agent-workbench/codex/sessions/resources/intellij.agent.workbench.codex.sessions.xml @@ -2,6 +2,7 @@ + diff --git a/plugins/agent-workbench/json/BUILD.bazel b/plugins/agent-workbench/json/BUILD.bazel index f8accde5a1ec..abee59dd1dc4 100644 --- a/plugins/agent-workbench/json/BUILD.bazel +++ b/plugins/agent-workbench/json/BUILD.bazel @@ -1,11 +1,18 @@ ### auto-generated section `build intellij.agent.workbench.json` start -load("@rules_jvm//:jvm.bzl", "jvm_library") +load("@rules_jvm//:jvm.bzl", "jvm_library", "resourcegroup") + +resourcegroup( + name = "json_resources", + srcs = glob(["resources/**/*"]), + strip_prefix = "resources" +) jvm_library( name = "json", module_name = "intellij.agent.workbench.json", visibility = ["//visibility:public"], srcs = glob(["src/**/*.kt", "src/**/*.java", "src/**/*.form"], allow_empty = True), + resources = [":json_resources"], deps = [ "@lib//:kotlin-stdlib", "//libraries/jackson/jackson", diff --git a/plugins/agent-workbench/json/intellij.agent.workbench.json.iml b/plugins/agent-workbench/json/intellij.agent.workbench.json.iml index 31be22a62084..03d91ce79088 100644 --- a/plugins/agent-workbench/json/intellij.agent.workbench.json.iml +++ b/plugins/agent-workbench/json/intellij.agent.workbench.json.iml @@ -3,6 +3,7 @@ + diff --git a/plugins/agent-workbench/json/resources/intellij.agent.workbench.json.xml b/plugins/agent-workbench/json/resources/intellij.agent.workbench.json.xml new file mode 100644 index 000000000000..6fb76a66265d --- /dev/null +++ b/plugins/agent-workbench/json/resources/intellij.agent.workbench.json.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/plugins/agent-workbench/plugin/plugin-content.yaml b/plugins/agent-workbench/plugin/plugin-content.yaml index 0d0687416c1f..79e720c1c158 100644 --- a/plugins/agent-workbench/plugin/plugin-content.yaml +++ b/plugins/agent-workbench/plugin/plugin-content.yaml @@ -16,6 +16,9 @@ - name: lib/modules/intellij.agent.workbench.codex.sessions.jar contentModules: - name: intellij.agent.workbench.codex.sessions +- name: lib/modules/intellij.agent.workbench.json.jar + contentModules: + - name: intellij.agent.workbench.json - name: lib/modules/intellij.agent.workbench.sessions.jar contentModules: - - name: intellij.agent.workbench.sessions + - name: intellij.agent.workbench.sessions \ No newline at end of file diff --git a/plugins/agent-workbench/plugin/resources/META-INF/plugin.xml b/plugins/agent-workbench/plugin/resources/META-INF/plugin.xml index 3326c1aacb72..0b4a06509422 100644 --- a/plugins/agent-workbench/plugin/resources/META-INF/plugin.xml +++ b/plugins/agent-workbench/plugin/resources/META-INF/plugin.xml @@ -5,6 +5,7 @@ Agent Workbench - AI-based developement for IntelliJ JetBrains + diff --git a/plugins/agent-workbench/sessions/resources/intellij.agent.workbench.sessions.xml b/plugins/agent-workbench/sessions/resources/intellij.agent.workbench.sessions.xml index 65c2c98c28a5..7dd14fd8a30f 100644 --- a/plugins/agent-workbench/sessions/resources/intellij.agent.workbench.sessions.xml +++ b/plugins/agent-workbench/sessions/resources/intellij.agent.workbench.sessions.xml @@ -39,6 +39,7 @@ + - + \ No newline at end of file diff --git a/plugins/agent-workbench/sessions/resources/messages/AgentSessionsBundle.properties b/plugins/agent-workbench/sessions/resources/messages/AgentSessionsBundle.properties index cc0f571ee98a..d8e42722c9c8 100644 --- a/plugins/agent-workbench/sessions/resources/messages/AgentSessionsBundle.properties +++ b/plugins/agent-workbench/sessions/resources/messages/AgentSessionsBundle.properties @@ -23,6 +23,7 @@ toolwindow.provider.codex=Codex toolwindow.provider.claude=Claude action.AgentWorkbenchSessions.Refresh.text=Refresh action.AgentWorkbenchSessions.ToggleDedicatedFrame.text=Open Chat in Dedicated Frame +action.AgentWorkbenchSessions.ToggleClaudeQuotaWidget.text=Show Claude Usage in Status Bar action.AgentWorkbenchSessions.ActivateWithProjectShortcut.text=Agent Threads advanced.settings.agent.workbench=Agent Workbench advanced.setting.agent.workbench.chat.open.in.dedicated.frame=Open Agent chat in dedicated frame @@ -46,3 +47,7 @@ status.bar.claude.quota.display.name=Claude Usage status.bar.claude.quota.text={0}% of {1} status.bar.claude.quota.tooltip.session=Session (5h): {0}% \u2014 resets {1} status.bar.claude.quota.tooltip.weekly=Weekly (7d): {0}% \u2014 resets {1} +toolwindow.claude.quota.hint.title=See Claude usage in your status bar +toolwindow.claude.quota.hint.body=Track 5h and 7d quota at a glance while you work. +toolwindow.claude.quota.hint.enable=Enable +toolwindow.claude.quota.hint.dismiss=Dismiss diff --git a/plugins/agent-workbench/sessions/src/AgentSessionsService.kt b/plugins/agent-workbench/sessions/src/AgentSessionsService.kt index 16674733dbb0..a88e3df1c5ca 100644 --- a/plugins/agent-workbench/sessions/src/AgentSessionsService.kt +++ b/plugins/agent-workbench/sessions/src/AgentSessionsService.kt @@ -541,6 +541,7 @@ internal class AgentSessionsService private constructor( fun openChatThread(path: String, thread: AgentSessionThread, currentProject: Project? = null) { val normalized = normalizePath(path) val key = buildOpenThreadActionKey(path = normalized, thread = thread) + markClaudeQuotaHintEligible(thread.provider) actionGate.launch( scope = serviceScope, key = key, @@ -565,6 +566,7 @@ internal class AgentSessionsService private constructor( fun openChatSubAgent(path: String, thread: AgentSessionThread, subAgent: AgentSubAgent, currentProject: Project? = null) { val normalized = normalizePath(path) val key = buildOpenSubAgentActionKey(path = normalized, thread = thread, subAgent = subAgent) + markClaudeQuotaHintEligible(thread.provider) actionGate.launch( scope = serviceScope, key = key, @@ -586,6 +588,7 @@ internal class AgentSessionsService private constructor( ) { val normalized = normalizePath(path) val key = buildCreateSessionActionKey(normalized, provider, mode) + markClaudeQuotaHintEligible(provider) actionGate.launch( scope = serviceScope, key = key, @@ -899,6 +902,13 @@ internal class AgentSessionsService private constructor( return "$OPEN_PROJECT_ACTION_KEY_PREFIX:$path" } + private fun markClaudeQuotaHintEligible(provider: AgentSessionProvider) { + if (provider != AgentSessionProvider.CLAUDE) { + return + } + service().markClaudeQuotaHintEligible() + } + private fun buildCreateSessionActionKey(path: String, provider: AgentSessionProvider, mode: AgentSessionLaunchMode): String { return "$CREATE_SESSION_ACTION_KEY_PREFIX:$path:$provider:mode=$mode" } diff --git a/plugins/agent-workbench/sessions/src/AgentSessionsToggleClaudeQuotaWidgetAction.kt b/plugins/agent-workbench/sessions/src/AgentSessionsToggleClaudeQuotaWidgetAction.kt new file mode 100644 index 000000000000..3bd2a0753b53 --- /dev/null +++ b/plugins/agent-workbench/sessions/src/AgentSessionsToggleClaudeQuotaWidgetAction.kt @@ -0,0 +1,17 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.agent.workbench.sessions + +import com.intellij.agent.workbench.sessions.claude.ClaudeQuotaStatusBarWidgetSettings +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.project.DumbAwareToggleAction + +internal class AgentSessionsToggleClaudeQuotaWidgetAction : DumbAwareToggleAction() { + override fun isSelected(e: AnActionEvent): Boolean = ClaudeQuotaStatusBarWidgetSettings.isEnabled() + + override fun setSelected(e: AnActionEvent, state: Boolean) { + ClaudeQuotaStatusBarWidgetSettings.setEnabled(state) + } + + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT +} diff --git a/plugins/agent-workbench/sessions/src/AgentSessionsToolWindow.kt b/plugins/agent-workbench/sessions/src/AgentSessionsToolWindow.kt index 896428c5abd0..9fbe2800d9a9 100644 --- a/plugins/agent-workbench/sessions/src/AgentSessionsToolWindow.kt +++ b/plugins/agent-workbench/sessions/src/AgentSessionsToolWindow.kt @@ -3,11 +3,14 @@ package com.intellij.agent.workbench.sessions // @spec community/plugins/agent-workbench/spec/agent-sessions.spec.md // @spec community/plugins/agent-workbench/spec/agent-sessions-thread-visibility.spec.md +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -15,12 +18,17 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.unit.dp import com.intellij.agent.workbench.chat.AgentChatTabSelectionService +import com.intellij.agent.workbench.sessions.claude.ClaudeQuotaStatusBarWidgetSettings import com.intellij.openapi.components.service import com.intellij.openapi.project.Project +import kotlinx.coroutines.delay import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.ui.component.OutlinedButton import org.jetbrains.jewel.ui.component.Text +import kotlin.time.Duration.Companion.seconds @Composable internal fun agentSessionsToolWindow(currentProject: Project) { @@ -30,11 +38,27 @@ internal fun agentSessionsToolWindow(currentProject: Project) { val state by service.state.collectAsState() val selectedChatTab by chatSelectionService.selectedChatTab.collectAsState() val lastUsedProvider by uiStateService.lastUsedProviderFlow.collectAsState() + val claudeQuotaHintEligible by uiStateService.claudeQuotaHintEligibleFlow.collectAsState() + val claudeQuotaHintAcknowledged by uiStateService.claudeQuotaHintAcknowledgedFlow.collectAsState() + val isClaudeQuotaWidgetEnabled by ClaudeQuotaStatusBarWidgetSettings.enabledFlow.collectAsState() LaunchedEffect(Unit) { service.refresh() } + LaunchedEffect(Unit) { + while (true) { + ClaudeQuotaStatusBarWidgetSettings.syncEnabledState() + delay(1.seconds) + } + } + + LaunchedEffect(claudeQuotaHintEligible, claudeQuotaHintAcknowledged, isClaudeQuotaWidgetEnabled) { + if (claudeQuotaHintEligible && !claudeQuotaHintAcknowledged && isClaudeQuotaWidgetEnabled) { + uiStateService.acknowledgeClaudeQuotaHint() + } + } + LaunchedEffect(selectedChatTab, state.projects) { val currentSelection = selectedChatTab ?: return@LaunchedEffect val identity = parseAgentSessionIdentity(currentSelection.threadIdentity) ?: return@LaunchedEffect @@ -66,6 +90,14 @@ internal fun agentSessionsToolWindow(currentProject: Project) { visibleThreadCounts = state.visibleThreadCounts, onShowMoreThreads = { path -> service.showMoreThreads(path) }, selectedTreeId = selectedTreeId, + showClaudeQuotaHint = claudeQuotaHintEligible && !claudeQuotaHintAcknowledged && !isClaudeQuotaWidgetEnabled, + onEnableClaudeQuotaWidget = { + ClaudeQuotaStatusBarWidgetSettings.setEnabled(true) + uiStateService.acknowledgeClaudeQuotaHint() + }, + onDismissClaudeQuotaHint = { + uiStateService.acknowledgeClaudeQuotaHint() + }, ) } @@ -86,6 +118,9 @@ internal fun agentSessionsToolWindowContent( visibleThreadCounts: Map = emptyMap(), onShowMoreThreads: (String) -> Unit = {}, selectedTreeId: SessionTreeId? = null, + showClaudeQuotaHint: Boolean = false, + onEnableClaudeQuotaWidget: () -> Unit = {}, + onDismissClaudeQuotaHint: () -> Unit = {}, ) { Column( modifier = Modifier @@ -93,6 +128,13 @@ internal fun agentSessionsToolWindowContent( .padding(horizontal = 10.dp, vertical = 12.dp), verticalArrangement = Arrangement.spacedBy(10.dp), ) { + if (showClaudeQuotaHint) { + claudeQuotaHintBanner( + onEnableClaudeQuotaWidget = onEnableClaudeQuotaWidget, + onDismiss = onDismissClaudeQuotaHint, + ) + } + when { state.projects.isEmpty() -> emptyState(isLoading = state.lastUpdatedAt == null) else -> sessionTree( @@ -116,6 +158,44 @@ internal fun agentSessionsToolWindowContent( } } +@Composable +private fun claudeQuotaHintBanner( + onEnableClaudeQuotaWidget: () -> Unit, + onDismiss: () -> Unit, +) { + val shape = RoundedCornerShape(8.dp) + val borderColor = JewelTheme.globalColors.borders.normal + .takeOrElse { JewelTheme.globalColors.text.disabled } + .copy(alpha = 0.35f) + val bodyColor = JewelTheme.globalColors.text.normal.copy(alpha = 0.84f) + + Column( + modifier = Modifier + .fillMaxWidth() + .border(width = 1.dp, color = borderColor, shape = shape) + .padding(10.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text( + text = AgentSessionsBundle.message("toolwindow.claude.quota.hint.title"), + style = AgentSessionsTextStyles.projectTitle(), + ) + Text( + text = AgentSessionsBundle.message("toolwindow.claude.quota.hint.body"), + style = AgentSessionsTextStyles.threadTitle(), + color = bodyColor, + ) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + OutlinedButton(onClick = onEnableClaudeQuotaWidget) { + Text(AgentSessionsBundle.message("toolwindow.claude.quota.hint.enable")) + } + OutlinedButton(onClick = onDismiss) { + Text(AgentSessionsBundle.message("toolwindow.claude.quota.hint.dismiss")) + } + } + } +} + @Composable private fun emptyState(isLoading: Boolean) { val messageKey = if (isLoading) "toolwindow.loading" else "toolwindow.empty.global" diff --git a/plugins/agent-workbench/sessions/src/AgentSessionsTreeUiStateService.kt b/plugins/agent-workbench/sessions/src/AgentSessionsTreeUiStateService.kt index af8c076a1bc2..4b663ce029b7 100644 --- a/plugins/agent-workbench/sessions/src/AgentSessionsTreeUiStateService.kt +++ b/plugins/agent-workbench/sessions/src/AgentSessionsTreeUiStateService.kt @@ -114,6 +114,10 @@ internal class AgentSessionsTreeUiStateService private val _lastUsedProviderFlow = MutableStateFlow(getLastUsedProvider()) val lastUsedProviderFlow: StateFlow = _lastUsedProviderFlow.asStateFlow() + private val _claudeQuotaHintEligibleFlow = MutableStateFlow(state.claudeQuotaHintEligible) + val claudeQuotaHintEligibleFlow: StateFlow = _claudeQuotaHintEligibleFlow.asStateFlow() + private val _claudeQuotaHintAcknowledgedFlow = MutableStateFlow(state.claudeQuotaHintAcknowledged) + val claudeQuotaHintAcknowledgedFlow: StateFlow = _claudeQuotaHintAcknowledgedFlow.asStateFlow() override fun isProjectCollapsed(path: String): Boolean { return normalizeSessionsProjectPath(path) in state.collapsedProjectPaths @@ -240,9 +244,23 @@ internal class AgentSessionsTreeUiStateService _lastUsedProviderFlow.value = provider } + fun markClaudeQuotaHintEligible() { + if (state.claudeQuotaHintEligible) return + updateState { it.copy(claudeQuotaHintEligible = true) } + _claudeQuotaHintEligibleFlow.value = true + } + + fun acknowledgeClaudeQuotaHint() { + if (state.claudeQuotaHintAcknowledged) return + updateState { it.copy(claudeQuotaHintAcknowledged = true) } + _claudeQuotaHintAcknowledgedFlow.value = true + } + override fun loadState(state: SessionsTreeUiStateState) { super.loadState(state) _lastUsedProviderFlow.value = state.lastUsedProvider?.let(AgentSessionProvider::fromOrNull) + _claudeQuotaHintEligibleFlow.value = state.claudeQuotaHintEligible + _claudeQuotaHintAcknowledgedFlow.value = state.claudeQuotaHintAcknowledged } @Serializable @@ -251,6 +269,8 @@ internal class AgentSessionsTreeUiStateService @JvmField val visibleThreadCountByProject: Map = emptyMap(), @JvmField val openProjectThreadPreviewsByProject: Map> = emptyMap(), @JvmField val lastUsedProvider: String? = null, + @JvmField val claudeQuotaHintEligible: Boolean = false, + @JvmField val claudeQuotaHintAcknowledged: Boolean = false, ) @Serializable diff --git a/plugins/agent-workbench/sessions/src/claude/ClaudeQuotaStatusBarWidget.kt b/plugins/agent-workbench/sessions/src/claude/ClaudeQuotaStatusBarWidget.kt index 483e342dddea..76f6ed28a122 100644 --- a/plugins/agent-workbench/sessions/src/claude/ClaudeQuotaStatusBarWidget.kt +++ b/plugins/agent-workbench/sessions/src/claude/ClaudeQuotaStatusBarWidget.kt @@ -26,17 +26,17 @@ import javax.swing.JPanel import javax.swing.JProgressBar import javax.swing.Timer -private const val WIDGET_ID = "claude.quota" +internal const val CLAUDE_QUOTA_WIDGET_ID = "claude.quota" private const val UI_REFRESH_INTERVAL_MS = 5_000 private const val UI_REFRESH_INITIAL_DELAY_MS = 1_000 private const val WARNING_QUOTA_PERCENT = 80 internal class ClaudeQuotaStatusBarWidgetFactory : StatusBarWidgetFactory { - override fun getId(): String = WIDGET_ID + override fun getId(): String = CLAUDE_QUOTA_WIDGET_ID override fun getDisplayName(): String = AgentSessionsBundle.message("status.bar.claude.quota.display.name") - override fun isEnabledByDefault(): Boolean = true + override fun isEnabledByDefault(): Boolean = false override fun createWidget(project: Project): StatusBarWidget = ClaudeQuotaStatusBarWidget() } @@ -62,7 +62,7 @@ internal class ClaudeQuotaStatusBarWidget : CustomStatusBarWidget, Activatable { override fun getPresentation(): StatusBarWidget.WidgetPresentation? = null - override fun ID(): String = WIDGET_ID + override fun ID(): String = CLAUDE_QUOTA_WIDGET_ID override fun getComponent(): JComponent = myComponent.get() diff --git a/plugins/agent-workbench/sessions/src/claude/ClaudeQuotaStatusBarWidgetSettings.kt b/plugins/agent-workbench/sessions/src/claude/ClaudeQuotaStatusBarWidgetSettings.kt new file mode 100644 index 000000000000..cae868390e4e --- /dev/null +++ b/plugins/agent-workbench/sessions/src/claude/ClaudeQuotaStatusBarWidgetSettings.kt @@ -0,0 +1,48 @@ +// 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.claude + +import com.intellij.openapi.components.service +import com.intellij.openapi.project.ProjectManager +import com.intellij.openapi.wm.StatusBarWidgetFactory +import com.intellij.openapi.wm.impl.status.widget.StatusBarWidgetSettings +import com.intellij.openapi.wm.impl.status.widget.StatusBarWidgetsManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +internal object ClaudeQuotaStatusBarWidgetSettings { + private val _enabledFlow = MutableStateFlow(readEnabledFromSettings()) + val enabledFlow: StateFlow = _enabledFlow.asStateFlow() + + fun isEnabled(): Boolean { + syncEnabledState() + return _enabledFlow.value + } + + fun syncEnabledState() { + _enabledFlow.value = readEnabledFromSettings() + } + + fun setEnabled(enabled: Boolean) { + val factory = findFactory() ?: return + val settings = StatusBarWidgetSettings.getInstance() + if (settings.isEnabled(factory) == enabled) { + return + } + + settings.setEnabled(factory, enabled) + for (project in ProjectManager.getInstance().openProjects) { + project.service().updateWidget(factory) + } + _enabledFlow.value = enabled + } + + private fun readEnabledFromSettings(): Boolean { + val factory = findFactory() ?: return false + return StatusBarWidgetSettings.getInstance().isEnabled(factory) + } + + private fun findFactory(): StatusBarWidgetFactory? { + return StatusBarWidgetFactory.EP_NAME.extensionList.firstOrNull { it.id == CLAUDE_QUOTA_WIDGET_ID } + } +} diff --git a/plugins/agent-workbench/sessions/testSrc/AgentSessionsClaudeQuotaWidgetActionRegistrationTest.kt b/plugins/agent-workbench/sessions/testSrc/AgentSessionsClaudeQuotaWidgetActionRegistrationTest.kt new file mode 100644 index 000000000000..5a9e0b4e0f62 --- /dev/null +++ b/plugins/agent-workbench/sessions/testSrc/AgentSessionsClaudeQuotaWidgetActionRegistrationTest.kt @@ -0,0 +1,18 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.agent.workbench.sessions + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class AgentSessionsClaudeQuotaWidgetActionRegistrationTest { + @Test + fun gearActionsContainClaudeQuotaWidgetToggle() { + val descriptor = checkNotNull(javaClass.classLoader.getResource("intellij.agent.workbench.sessions.xml")) { + "Module descriptor intellij.agent.workbench.sessions.xml is missing" + }.readText() + + assertThat(descriptor) + .contains("") + .contains("