feat(agent-rows): make conversation names the default

Always prefer a usable conversation name for sidebar and dashboard agent rows, falling back to the last message when the resolver rejects a title. Remove the preference, settings UI, search copy, and catalog entries. Keep subagent labels unchanged and reject generic Terminal N placeholders so partial tab snapshots cannot hide meaningful prompts.
This commit is contained in:
Brennan Benson
2026-07-22 17:42:17 -07:00
parent 9d6d4f4e0d
commit b75ab61575
14 changed files with 29 additions and 168 deletions
@@ -34,21 +34,12 @@ beforeEach(() => {
})
describe('useAgentRowConversationName', () => {
it('returns null without reading the tab map while the setting is off', () => {
const tabsByWorktree = new Proxy(
{},
{
get: () => {
throw new Error('inactive rows must not read the tab map')
}
}
)
storeState.current = { settings: {}, tabsByWorktree }
expect(useAgentRowConversationName(makeAgent())).toBeNull()
it('returns the conversation name by default', () => {
expect(useAgentRowConversationName(makeAgent())).toBe('Patient sync spike')
})
it('returns the conversation name when the setting is on', () => {
storeState.current = { settings: { agentRowsUseConversationName: true }, tabsByWorktree: {} }
it('ignores a retired stored opt-out value', () => {
storeState.current = { settings: { agentRowsUseConversationName: false }, tabsByWorktree: {} }
expect(useAgentRowConversationName(makeAgent())).toBe('Patient sync spike')
})
@@ -61,7 +52,7 @@ describe('useAgentRowConversationName', () => {
}
}
)
storeState.current = { settings: { agentRowsUseConversationName: true }, tabsByWorktree }
storeState.current = { settings: {}, tabsByWorktree }
expect(useAgentRowConversationName(makeAgent({ rowSource: 'subagent' }))).toBeNull()
})
@@ -82,7 +73,7 @@ describe('useAgentRowConversationName', () => {
}
)
storeState.current = {
settings: { agentRowsUseConversationName: true },
settings: {},
tabsByWorktree: { 'wt-1': tabs }
}
@@ -102,7 +93,7 @@ describe('useAgentRowConversationName', () => {
it('prefers the live store tab over the stale row snapshot', () => {
storeState.current = {
settings: { agentRowsUseConversationName: true },
settings: {},
// Why: row data patches entries in place and keeps the creation-time tab
// snapshot; a rename landing after that must still surface.
tabsByWorktree: {
@@ -116,10 +107,10 @@ describe('useAgentRowConversationName', () => {
const agent = makeAgent({
tab: { customTitle: null, title: '', generatedTitle: 'Fix intake flow' }
} as Partial<DashboardAgentRow>)
storeState.current = { settings: { agentRowsUseConversationName: true }, tabsByWorktree: {} }
storeState.current = { settings: {}, tabsByWorktree: {} }
expect(useAgentRowConversationName(agent)).toBeNull()
storeState.current = {
settings: { agentRowsUseConversationName: true, tabAutoGenerateTitle: true },
settings: { tabAutoGenerateTitle: true },
tabsByWorktree: {}
}
expect(useAgentRowConversationName(agent)).toBe('Fix intake flow')
@@ -22,21 +22,19 @@ function getIndexedTab(
return tabIndex.get(tabId)
}
/** The row's conversation name, or null when the mode is off or nothing usable exists. */
/** The row's conversation name, or null when nothing usable exists. */
export function useAgentRowConversationName(agent: DashboardAgentRow): string | null {
const mode = useAppStore((s) => {
if (agent.rowSource === 'subagent' || s.settings?.agentRowsUseConversationName !== true) {
return 0
}
return s.settings?.tabAutoGenerateTitle === true ? 2 : 1
})
// Why: the opt-in gate keeps inactive rows off the hot tab map entirely.
const liveTab = useAppStore((s) =>
mode === 0 ? undefined : getIndexedTab(s.tabsByWorktree[agent.tab.worktreeId], agent.tab.id)
const isSubagent = agent.rowSource === 'subagent'
const generatedTitlesEnabled = useAppStore(
(s) => !isSubagent && s.settings?.tabAutoGenerateTitle === true
)
if (mode === 0) {
// Why: child rows describe the child, so the parent tab would mislabel them.
const liveTab = useAppStore((s) =>
isSubagent ? undefined : getIndexedTab(s.tabsByWorktree[agent.tab.worktreeId], agent.tab.id)
)
if (isSubagent) {
return null
}
// Why: retained row snapshots need a fallback after their live tab disappears.
return getAgentRowConversationName(liveTab ?? agent.tab, agent.agentType, mode === 2)
return getAgentRowConversationName(liveTab ?? agent.tab, agent.agentType, generatedTitlesEnabled)
}
@@ -22,10 +22,6 @@ import {
getAgentGeneratedTabTitlesDescription,
getAgentGeneratedTabTitlesTitle
} from './agent-generated-tab-title-copy'
import {
getAgentRowConversationNamesDescription,
getAgentRowConversationNamesTitle
} from './agent-row-conversation-name-copy'
import { getAgentStatusHooksDescription, getAgentStatusHooksTitle } from './agent-status-hooks-copy'
import {
SettingsBadge,
@@ -837,8 +833,6 @@ export function AgentsPane({
<AgentGeneratedTabTitlesSetting settings={settings} updateSettings={updateSettings} />
<AgentRowConversationNamesSetting settings={settings} updateSettings={updateSettings} />
<AgentAwakeSetting settings={settings} updateSettings={updateSettings} />
<AgentCacheTimerSection settings={settings} updateSettings={updateSettings} />
@@ -1009,25 +1003,3 @@ export function AgentGeneratedTabTitlesSetting({
</section>
)
}
export function AgentRowConversationNamesSetting({
settings,
updateSettings
}: AgentsPaneProps): React.JSX.Element {
const enabled = settings.agentRowsUseConversationName === true
return (
<section className="space-y-3">
<SettingsSwitchRow
label={getAgentRowConversationNamesTitle()}
description={getAgentRowConversationNamesDescription()}
checked={enabled}
onChange={() =>
updateSettings({
agentRowsUseConversationName: !enabled
})
}
ariaLabel={getAgentRowConversationNamesTitle()}
/>
</section>
)
}
@@ -1,35 +0,0 @@
import { translate } from '@/i18n/i18n'
import { searchKeywords } from './settings-search-keywords'
export function getAgentRowConversationNamesTitle(): string {
return translate(
'auto.components.settings.agent-row-conversation-name-copy.title',
'Label agent rows with conversation names'
)
}
export function getAgentRowConversationNamesDescription(): string {
return translate(
'auto.components.settings.agent-row-conversation-name-copy.description',
'Show conversation names on sidebar and dashboard agent rows instead of the last message. Rows without a usable name keep the last message.'
)
}
export function getAgentRowConversationNamesSearchKeywords(): string[] {
return searchKeywords([
{ key: 'auto.components.settings.agents.search.96ba2373b6', fallback: 'agent' },
{ key: 'auto.components.settings.agents.search.conversation', fallback: 'conversation' },
{ key: 'auto.components.settings.agents.search.966890236d', fallback: 'name' },
{
key: 'auto.components.settings.agents.search.conversationName',
fallback: 'conversation name'
},
{ key: 'auto.components.settings.agents.search.sidebar', fallback: 'sidebar' },
{ key: 'auto.components.settings.agents.search.dashboard', fallback: 'dashboard' },
{ key: 'auto.components.settings.agents.search.activityRow', fallback: 'activity' },
{ key: 'auto.components.settings.agents.search.lastMessage', fallback: 'last message' },
{ key: 'auto.components.settings.agents.search.c64059f50d', fallback: 'prompt' },
{ key: 'auto.components.settings.agents.search.5784ae8c43', fallback: 'rename' },
{ key: 'auto.components.settings.agents.search.rowLabel', fallback: 'label' }
])
}
@@ -9,11 +9,6 @@ import {
getAgentGeneratedTabTitlesSearchKeywords,
getAgentGeneratedTabTitlesTitle
} from './agent-generated-tab-title-copy'
import {
getAgentRowConversationNamesDescription,
getAgentRowConversationNamesSearchKeywords,
getAgentRowConversationNamesTitle
} from './agent-row-conversation-name-copy'
import {
getAgentStatusHooksDescription,
getAgentStatusHooksSearchKeywords,
@@ -116,11 +111,6 @@ const getAllAgentsPaneSearchEntries = createLocalizedCatalog(() => [
description: getAgentGeneratedTabTitlesDescription(),
keywords: getAgentGeneratedTabTitlesSearchKeywords()
},
{
title: getAgentRowConversationNamesTitle(),
description: getAgentRowConversationNamesDescription(),
keywords: getAgentRowConversationNamesSearchKeywords()
},
{
title: getAgentAwakeTitle(),
description: getAgentAwakeDescription(),
+1 -12
View File
@@ -7301,14 +7301,7 @@
"agentPermissions": "Agent Permissions",
"agentPermissionsDescription": "Switch agent permission defaults between Yolo and Manual.",
"agentRuntime": "Agent Runtime",
"agentRuntimeDescription": "Choose whether agents are detected and launched on Windows or in WSL by default.",
"conversation": "conversation",
"conversationName": "conversation name",
"sidebar": "sidebar",
"dashboard": "dashboard",
"activityRow": "activity",
"lastMessage": "last message",
"rowLabel": "label"
"agentRuntimeDescription": "Choose whether agents are detected and launched on Windows or in WSL by default."
}
},
"appearance": {
@@ -9315,10 +9308,6 @@
"title": "Editor Font Family",
"description": "Font used by file editors and diff views. Leave empty to follow the terminal font.",
"placeholder": "Same as terminal font"
},
"agent-row-conversation-name-copy": {
"title": "Label agent rows with conversation names",
"description": "Show conversation names on sidebar and dashboard agent rows instead of the last message. Rows without a usable name keep the last message."
}
},
"right": {
+1 -12
View File
@@ -7241,14 +7241,7 @@
"agentPermissions": "Permisos de agentes",
"agentPermissionsDescription": "Cambia los permisos predeterminados de los agentes entre Yolo y Manual.",
"agentRuntime": "Runtime del agente",
"agentRuntimeDescription": "Elige si los agentes se detectan y ejecutan en Windows o en WSL de forma predeterminada.",
"conversation": "conversation",
"conversationName": "conversation name",
"sidebar": "sidebar",
"dashboard": "dashboard",
"activityRow": "activity",
"lastMessage": "last message",
"rowLabel": "label"
"agentRuntimeDescription": "Elige si los agentes se detectan y ejecutan en Windows o en WSL de forma predeterminada."
}
},
"appearance": {
@@ -9292,10 +9285,6 @@
"title": "Editor Font Family",
"description": "Font used by file editors and diff views. Leave empty to follow the terminal font.",
"placeholder": "Same as terminal font"
},
"agent-row-conversation-name-copy": {
"title": "Label agent rows with conversation names",
"description": "Show conversation names on sidebar and dashboard agent rows instead of the last message. Rows without a usable name keep the last message."
}
},
"right": {
+1 -12
View File
@@ -7263,14 +7263,7 @@
"agentPermissions": "Agent Permissions",
"agentPermissionsDescription": "Switch agent permission defaults between Yolo and Manual.",
"agentRuntime": "Agent のランタイム",
"agentRuntimeDescription": "デフォルトで agents を Windows または WSL のどちらで検出および起動するかを選択します。",
"conversation": "conversation",
"conversationName": "conversation name",
"sidebar": "sidebar",
"dashboard": "dashboard",
"activityRow": "activity",
"lastMessage": "last message",
"rowLabel": "label"
"agentRuntimeDescription": "デフォルトで agents を Windows または WSL のどちらで検出および起動するかを選択します。"
}
},
"appearance": {
@@ -9292,10 +9285,6 @@
"title": "Editor Font Family",
"description": "Font used by file editors and diff views. Leave empty to follow the terminal font.",
"placeholder": "Same as terminal font"
},
"agent-row-conversation-name-copy": {
"title": "Label agent rows with conversation names",
"description": "Show conversation names on sidebar and dashboard agent rows instead of the last message. Rows without a usable name keep the last message."
}
},
"right": {
+1 -12
View File
@@ -7226,14 +7226,7 @@
"agentPermissions": "Agent 권한",
"agentPermissionsDescription": "agent 권한 기본값을 Yolo와 수동 사이에서 전환합니다.",
"agentRuntime": "Agent 런타임",
"agentRuntimeDescription": "기본적으로 Windows 또는 WSL에서 agents를 감지하고 시작할지 선택합니다.",
"conversation": "conversation",
"conversationName": "conversation name",
"sidebar": "sidebar",
"dashboard": "dashboard",
"activityRow": "activity",
"lastMessage": "last message",
"rowLabel": "label"
"agentRuntimeDescription": "기본적으로 Windows 또는 WSL에서 agents를 감지하고 시작할지 선택합니다."
}
},
"appearance": {
@@ -9292,10 +9285,6 @@
"title": "Editor Font Family",
"description": "Font used by file editors and diff views. Leave empty to follow the terminal font.",
"placeholder": "Same as terminal font"
},
"agent-row-conversation-name-copy": {
"title": "Label agent rows with conversation names",
"description": "Show conversation names on sidebar and dashboard agent rows instead of the last message. Rows without a usable name keep the last message."
}
},
"right": {
+1 -12
View File
@@ -7226,14 +7226,7 @@
"agentPermissions": "智能体权限",
"agentPermissionsDescription": "在 Yolo 和手动之间切换智能体权限默认值。",
"agentRuntime": "智能体运行时",
"agentRuntimeDescription": "选择默认在 Windows 还是 WSL 中检测并启动智能体。",
"conversation": "conversation",
"conversationName": "conversation name",
"sidebar": "sidebar",
"dashboard": "dashboard",
"activityRow": "activity",
"lastMessage": "last message",
"rowLabel": "label"
"agentRuntimeDescription": "选择默认在 Windows 还是 WSL 中检测并启动智能体。"
}
},
"appearance": {
@@ -9292,10 +9285,6 @@
"title": "Editor Font Family",
"description": "Font used by file editors and diff views. Leave empty to follow the terminal font.",
"placeholder": "Same as terminal font"
},
"agent-row-conversation-name-copy": {
"title": "Label agent rows with conversation names",
"description": "Show conversation names on sidebar and dashboard agent rows instead of the last message. Rows without a usable name keep the last message."
}
},
"right": {
@@ -116,6 +116,9 @@ describe('getAgentRowConversationName', () => {
it('rejects empty, glyph-only, and default terminal titles', () => {
expect(getAgentRowConversationName(makeTab(), 'claude', false)).toBeNull()
expect(getAgentRowConversationName(makeTab({ title: '✳' }), 'claude', false)).toBeNull()
expect(
getAgentRowConversationName(makeTab({ title: 'Terminal 1' }), 'claude', false)
).toBeNull()
expect(
getAgentRowConversationName(
makeTab({ title: 'Terminal 2', defaultTitle: 'Terminal 2' }),
@@ -38,6 +38,7 @@ const AGENT_IDENTITY_ALIASES_LOWER: Readonly<Record<string, readonly string[]>>
}
const STATUS_WITH_CONTEXT_RE = /^(?:ready|idle|done)(?:\s+\([^)]*\))?$/i
const DEFAULT_TERMINAL_TITLE_RE = /^terminal \d+$/i
function isIdentityStatusTitle(titleLower: string, identityLower: string): boolean {
return (
@@ -93,6 +94,7 @@ function conversationNameFromLiveTitle(
lower === FALLBACK_TAB_TITLE_LOWER ||
isAgentIdentityStatusTitle(lower, agentType, agentTypeLabelLower) ||
STATUS_WITH_CONTEXT_RE.test(stripped) ||
DEFAULT_TERMINAL_TITLE_RE.test(stripped) ||
isClaudeManagementTitle(stripped) ||
isCwdLikeTitle(stripped)
) {
-1
View File
@@ -321,7 +321,6 @@ export function getDefaultSettings(homedir: string): GlobalSettings {
agentYoloDefaultsMigrated: true,
agentStatusHooksEnabled: true,
tabAutoGenerateTitle: false,
agentRowsUseConversationName: false,
confirmClosePinnedTab: true,
keepComputerAwakeWhileAgentsRun: false,
// Why: 'auto' probes keyboard layout so non-US users can type Option chars like @/€/[ out of the box (issue #903). See src/renderer/src/lib/keyboard-layout/*.
-4
View File
@@ -2868,10 +2868,6 @@ export type GlobalSettings = {
dismissedSkillFreshnessNudges?: string[]
/** Why: generated tab titles are subjective, so they stay opt-in and manual renames win. */
tabAutoGenerateTitle: boolean
/** Sidebar/dashboard agent rows label with the conversation name (tab rename or
* agent-set title) instead of the last message; rows without a usable name
* keep the last-message label. Opt-in: the live prompt stays the default. */
agentRowsUseConversationName?: boolean
/** Why: pinned tabs can still be closed via keyboard/native-menu; this gates that behind a confirmation. Defaults on. */
confirmClosePinnedTab: boolean
/** When true, Orca requests local awake assertions while hook-reported agents are working. */