diff --git a/src/renderer/src/components/agent-session-continuation/AgentSessionContinuationDialog.test.tsx b/src/renderer/src/components/agent-session-continuation/AgentSessionContinuationDialog.test.tsx
index ac551fbc111..7a4265d3813 100644
--- a/src/renderer/src/components/agent-session-continuation/AgentSessionContinuationDialog.test.tsx
+++ b/src/renderer/src/components/agent-session-continuation/AgentSessionContinuationDialog.test.tsx
@@ -23,8 +23,17 @@ vi.mock('@/lib/agent-catalog', () => ({
getAgentLabel: () => 'Codex'
}))
vi.mock('@/components/agent/AgentCombobox', () => ({
- default: ({ value }: { value: string | null }) =>
- React.createElement('div', { 'data-agent': value ?? '' })
+ default: ({
+ value,
+ allowBlankTerminal
+ }: {
+ value: string | null
+ allowBlankTerminal?: boolean
+ }) =>
+ React.createElement('div', {
+ 'data-agent': value ?? '',
+ 'data-allow-blank': String(allowBlankTerminal === true)
+ })
}))
vi.mock('@/components/ui/dialog', () => ({
Dialog: ({ open, children }: { open: boolean; children?: ReactNode }) =>
@@ -105,5 +114,7 @@ describe('AgentSessionContinuationDialog', () => {
await act(async () => resolveSecond(['codex']))
await vi.waitFor(() => expect(container.querySelector('[data-agent="codex"]')).not.toBeNull())
+ // Why: continuing a chat needs an agent to resume it into.
+ expect(container.querySelector('[data-allow-blank="false"]')).not.toBeNull()
})
})
diff --git a/src/renderer/src/components/agent-session-continuation/AgentSessionContinuationDialog.tsx b/src/renderer/src/components/agent-session-continuation/AgentSessionContinuationDialog.tsx
index 96d802da2cd..9183212d814 100644
--- a/src/renderer/src/components/agent-session-continuation/AgentSessionContinuationDialog.tsx
+++ b/src/renderer/src/components/agent-session-continuation/AgentSessionContinuationDialog.tsx
@@ -200,12 +200,7 @@ export function AgentSessionContinuationDialog({
agents={agents}
value={selectedAgent}
onValueChange={setSelectedAgent}
- allowBlankTerminal={false}
allowNarrowTrigger
- emptyLabel={translate(
- 'components.agentSessionContinuation.selectAgent',
- 'Select an Agent'
- )}
triggerClassName="min-w-0 w-full"
/>
{detecting ? (
diff --git a/src/renderer/src/components/agent/AgentCombobox.test.tsx b/src/renderer/src/components/agent/AgentCombobox.test.tsx
index 2ba0a0e2144..d9dd02c751f 100644
--- a/src/renderer/src/components/agent/AgentCombobox.test.tsx
+++ b/src/renderer/src/components/agent/AgentCombobox.test.tsx
@@ -38,6 +38,7 @@ describe('AgentCombobox', () => {
agents={AGENT_CATALOG}
value={null}
onValueChange={vi.fn()}
+ allowBlankTerminal
defaultAgent="codex"
onSetDefault={onSetDefault}
/>
@@ -52,14 +53,7 @@ describe('AgentCombobox', () => {
it('does not offer a default action for an Agent-only empty state', () => {
const onSetDefault = vi.fn()
render(
-
+
)
fireEvent.contextMenu(screen.getByRole('combobox'))
@@ -162,21 +156,41 @@ describe('AgentCombobox', () => {
}
})
- it('supports an Agent-only empty state without presenting a blank terminal', () => {
+ it('defaults to an Agent-only empty state without presenting a blank terminal', () => {
const markup = renderToStaticMarkup(
-
+
)
expect(markup).toContain('Select an Agent')
expect(markup).not.toContain('Blank Terminal')
})
+ it('hides Blank Terminal from the list unless the caller opts in', () => {
+ render()
+
+ fireEvent.click(screen.getByRole('combobox'))
+
+ expect(screen.getByRole('option', { name: 'Claude' })).toBeTruthy()
+ expect(screen.queryByRole('option', { name: 'Blank Terminal' })).toBeNull()
+ })
+
+ it('offers Blank Terminal and reports null when the caller opts in', () => {
+ const onValueChange = vi.fn()
+ render(
+
+ )
+
+ fireEvent.click(screen.getByRole('combobox'))
+ fireEvent.click(screen.getByRole('option', { name: 'Blank Terminal' }))
+
+ expect(onValueChange).toHaveBeenCalledWith(null)
+ })
+
it('uses the bundled OpenClaude favicon crop instead of Claude or GitHub artwork', () => {
const markup = renderToStaticMarkup()
diff --git a/src/renderer/src/components/agent/AgentCombobox.tsx b/src/renderer/src/components/agent/AgentCombobox.tsx
index 1c2ef729db9..4d10363d721 100644
--- a/src/renderer/src/components/agent/AgentCombobox.tsx
+++ b/src/renderer/src/components/agent/AgentCombobox.tsx
@@ -32,11 +32,15 @@ import { translate } from '@/i18n/i18n'
type DefaultAgentPreference = TuiAgent | 'blank' | null
-type AgentComboboxProps = {
+// Why: Blank Terminal is opt-in so agent-only flows (automations, source-control
+// actions) never offer a choice they cannot run, and never receive null for it.
+type AgentComboboxSelectionProps =
+ | { allowBlankTerminal: true; onValueChange: (agent: TuiAgent | null) => void }
+ | { allowBlankTerminal?: false; onValueChange: (agent: TuiAgent) => void }
+
+type AgentComboboxProps = AgentComboboxSelectionProps & {
agents: AgentCatalogEntry[]
value: TuiAgent | null
- onValueChange: (agent: TuiAgent | null) => void
- onValueSelected?: (agent: TuiAgent | null) => void
onOpenManageAgents?: () => void
/** Current saved default agent preference. Used to render a subtle "default"
* indicator in the list and to tell which right-click menu item is the
@@ -51,8 +55,6 @@ type AgentComboboxProps = {
* field as the last keyboard-submit step. */
onTriggerEnter?: () => void
allowNarrowTrigger?: boolean
- allowBlankTerminal?: boolean
- emptyLabel?: string
}
const BLANK_VALUE = '__none__'
@@ -150,17 +152,15 @@ function renderItem({
export default function AgentCombobox({
agents,
value,
- onValueChange,
- onValueSelected,
onOpenManageAgents,
defaultAgent,
onSetDefault,
triggerClassName,
onTriggerEnter,
allowNarrowTrigger = false,
- allowBlankTerminal = true,
- emptyLabel
+ ...selection
}: AgentComboboxProps): React.JSX.Element {
+ const allowBlankTerminal = selection.allowBlankTerminal === true
const [open, setOpen] = useState(false)
const [query, setQuery] = useState('')
// Why: controlled cmdk selection so hovering the footer (which lives outside
@@ -251,15 +251,22 @@ export default function AgentCombobox({
[cancelFocusFrame, value]
)
- const handleSelect = useCallback(
- (nextValue: TuiAgent | null) => {
- onValueChange(nextValue)
- setOpen(false)
- setQuery('')
- onValueSelected?.(nextValue)
- },
- [onValueChange, onValueSelected]
- )
+ const closePicker = useCallback(() => {
+ setOpen(false)
+ setQuery('')
+ }, [])
+
+ const handleSelectAgent = (nextValue: TuiAgent): void => {
+ selection.onValueChange(nextValue)
+ closePicker()
+ }
+
+ const handleSelectBlank = (): void => {
+ if (selection.allowBlankTerminal) {
+ selection.onValueChange(null)
+ }
+ closePicker()
+ }
// Why: mirror RepoCombobox's trigger-keydown handling — the button-style
// trigger treats the current value as a confirmed selection. Plain focus does
@@ -344,8 +351,15 @@ export default function AgentCombobox({
}
label={
- emptyLabel ??
- translate('auto.components.agent.AgentCombobox.986f946354', 'Blank Terminal')
+ allowBlankTerminal
+ ? translate(
+ 'auto.components.agent.AgentCombobox.986f946354',
+ 'Blank Terminal'
+ )
+ : translate(
+ 'components.agentSessionContinuation.selectAgent',
+ 'Select an Agent'
+ )
}
/>
)}
@@ -388,7 +402,7 @@ export default function AgentCombobox({
itemValue: BLANK_VALUE,
isChecked: value === null,
isDefault: defaultAgent === 'blank',
- onSelect: () => handleSelect(null),
+ onSelect: handleSelectBlank,
onSetDefault: onSetDefault ? () => onSetDefault('blank') : undefined,
icon: ,
label: translate(
@@ -403,7 +417,7 @@ export default function AgentCombobox({
itemValue: agent.id,
isChecked: value === agent.id,
isDefault: defaultAgent === agent.id,
- onSelect: () => handleSelect(agent.id),
+ onSelect: () => handleSelectAgent(agent.id),
onSetDefault: onSetDefault ? () => onSetDefault(agent.id) : undefined,
icon: ,
label: agent.label
diff --git a/src/renderer/src/components/automations/AutomationEditorSettingsSidebar.test.tsx b/src/renderer/src/components/automations/AutomationEditorSettingsSidebar.test.tsx
new file mode 100644
index 00000000000..dd0d1d1650e
--- /dev/null
+++ b/src/renderer/src/components/automations/AutomationEditorSettingsSidebar.test.tsx
@@ -0,0 +1,80 @@
+// @vitest-environment happy-dom
+
+import { cleanup, fireEvent, render, screen } from '@testing-library/react'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { AGENT_CATALOG } from '@/lib/agent-catalog'
+import type { AutomationDraft } from './AutomationEditorDialog'
+import { AutomationEditorSettingsSidebar } from './AutomationEditorSettingsSidebar'
+
+// Why: only the Agent field is under test; the sibling fields own their own coverage.
+vi.mock('./AutomationProjectCombobox', () => ({ default: () => null }))
+vi.mock('./AutomationWorkspaceField', () => ({ AutomationWorkspaceField: () => null }))
+vi.mock('./AutomationSessionField', () => ({ AutomationSessionField: () => null }))
+vi.mock('./AutomationSchedulePicker', () => ({ AutomationSchedulePicker: () => null }))
+vi.mock('./AutomationMissedRunGraceField', () => ({ AutomationMissedRunGraceField: () => null }))
+vi.mock('./AutomationPrecheckFields', () => ({ AutomationPrecheckFields: () => null }))
+vi.mock('./AutomationSetupDecisionField', () => ({ AutomationSetupDecisionField: () => null }))
+vi.mock('./AutomationDestinationField', () => ({ AutomationDestinationField: () => null }))
+vi.mock('./AutomationExtraAgentArgsField', () => ({ AutomationExtraAgentArgsField: () => null }))
+
+const DRAFT: AutomationDraft = {
+ name: '',
+ prompt: '',
+ agentId: 'codex',
+ extraAgentArgs: '',
+ projectId: '',
+ workspaceMode: 'existing',
+ workspaceId: '',
+ baseBranch: '',
+ reuseSession: false,
+ precheckCommand: '',
+ precheckTimeoutSeconds: '30',
+ preset: 'weekly',
+ time: '09:15',
+ dayOfWeek: '5',
+ customSchedule: '',
+ missedRunGraceMinutes: '720',
+ savedSchedule: null,
+ scheduleWarning: null
+}
+
+function renderSidebar(onDraftChange: (updater: (d: AutomationDraft) => AutomationDraft) => void) {
+ return render(
+ repoId}
+ repoMap={new Map()}
+ worktrees={[]}
+ settings={null}
+ draft={DRAFT}
+ visibleAgents={AGENT_CATALOG}
+ pickerTriggerClassName=""
+ segmentedGroupClassName=""
+ segmentedItemClassName=""
+ onProjectChange={vi.fn()}
+ onDraftChange={onDraftChange}
+ onSetupDecisionTouched={vi.fn()}
+ />
+ )
+}
+
+afterEach(cleanup)
+
+describe('AutomationEditorSettingsSidebar agent picker', () => {
+ it('offers agents only, never Blank Terminal', () => {
+ const onDraftChange = vi.fn()
+ renderSidebar(onDraftChange)
+
+ fireEvent.click(screen.getByRole('combobox'))
+
+ expect(screen.queryByRole('option', { name: 'Blank Terminal' })).toBeNull()
+ fireEvent.click(screen.getByRole('option', { name: 'Claude' }))
+ expect(onDraftChange).toHaveBeenCalledTimes(1)
+ const updater = onDraftChange.mock.calls[0][0]
+ expect(updater(DRAFT).agentId).toBe('claude')
+ })
+})
diff --git a/src/renderer/src/components/automations/AutomationEditorSettingsSidebar.tsx b/src/renderer/src/components/automations/AutomationEditorSettingsSidebar.tsx
index ff73ddd116f..e468f5ba4dd 100644
--- a/src/renderer/src/components/automations/AutomationEditorSettingsSidebar.tsx
+++ b/src/renderer/src/components/automations/AutomationEditorSettingsSidebar.tsx
@@ -102,9 +102,7 @@ export function AutomationEditorSettingsSidebar({
- agentId && onDraftChange((current) => ({ ...current, agentId }))
- }
+ onValueChange={(agentId) => onDraftChange((current) => ({ ...current, agentId }))}
defaultAgent={settings?.defaultTuiAgent ?? null}
triggerClassName={`h-9 w-full min-w-0 ${pickerTriggerClassName}`}
allowNarrowTrigger
diff --git a/src/renderer/src/components/new-workspace/NewWorkspaceComposerAgentSection.test.tsx b/src/renderer/src/components/new-workspace/NewWorkspaceComposerAgentSection.test.tsx
new file mode 100644
index 00000000000..eff02a5f233
--- /dev/null
+++ b/src/renderer/src/components/new-workspace/NewWorkspaceComposerAgentSection.test.tsx
@@ -0,0 +1,36 @@
+// @vitest-environment happy-dom
+
+import { cleanup, fireEvent, render, screen } from '@testing-library/react'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { TooltipProvider } from '@/components/ui/tooltip'
+import { AGENT_CATALOG } from '@/lib/agent-catalog'
+import { NewWorkspaceComposerAgentSection } from './NewWorkspaceComposerAgentSection'
+
+afterEach(cleanup)
+
+describe('NewWorkspaceComposerAgentSection agent picker', () => {
+ it('offers Blank Terminal because a workspace can start without an agent', () => {
+ const onQuickAgentChange = vi.fn()
+ render(
+
+
+
+ )
+
+ fireEvent.click(screen.getByRole('combobox'))
+ fireEvent.click(screen.getByRole('option', { name: 'Blank Terminal' }))
+
+ expect(onQuickAgentChange).toHaveBeenCalledWith(null)
+ })
+})
diff --git a/src/renderer/src/components/new-workspace/NewWorkspaceComposerAgentSection.tsx b/src/renderer/src/components/new-workspace/NewWorkspaceComposerAgentSection.tsx
index 4d597d3d436..5f790a5748e 100644
--- a/src/renderer/src/components/new-workspace/NewWorkspaceComposerAgentSection.tsx
+++ b/src/renderer/src/components/new-workspace/NewWorkspaceComposerAgentSection.tsx
@@ -71,6 +71,7 @@ export function NewWorkspaceComposerAgentSection({
agents={visibleQuickAgents}
value={quickAgent}
onValueChange={onQuickAgentChange}
+ allowBlankTerminal
onOpenManageAgents={onOpenAgentSettings}
defaultAgent={defaultTuiAgent}
onSetDefault={handleSetDefaultAgent}
diff --git a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.test.tsx b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.test.tsx
index 5d6e784b00c..952ee92494c 100644
--- a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.test.tsx
+++ b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.test.tsx
@@ -6,8 +6,17 @@ import type { GlobalSettings } from '../../../../shared/global-settings-types'
import type { Repo } from '../../../../shared/repo-types'
vi.mock('@/components/agent/AgentCombobox', () => ({
- default: ({ value }: { value: string | null }) =>
- React.createElement('div', { 'data-agent-value': value ?? '' })
+ default: ({
+ value,
+ allowBlankTerminal
+ }: {
+ value: string | null
+ allowBlankTerminal?: boolean
+ }) =>
+ React.createElement('div', {
+ 'data-agent-value': value ?? '',
+ 'data-allow-blank': String(allowBlankTerminal === true)
+ })
}))
vi.mock('@/components/ui/dialog', () => ({
@@ -110,6 +119,10 @@ describe('SourceControlAgentActionDialogForm', () => {
expect(markup).toContain('Resolve the merge conflicts reported for this pull request.')
})
+ it('offers agents only, since an action cannot start without one', () => {
+ expect(renderForm()).toContain('data-allow-blank="false"')
+ })
+
it('checks already-saved copy against the selected save target', () => {
const settings = settingsWithSavedGlobalRecipe()
const saveTargets = [