mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 08:03:28 +00:00
feat(agent-rows): opt-in conversation-name labels for agent rows
Sidebar worktree-card and dashboard agent rows always show the last message sent to the agent, so rows relabel on every turn and a 'continue' prompt becomes the row's name. Add an opt-in Agents setting that labels rows with the conversation name instead, resolved with the tab bar's precedence: manual rename, quick-command label, OpenCode session title, generated title (behind its existing setting), then the agent-set live title. Live titles count only when they carry a real name - status decoration is stripped, and pure status, identity-echo, spinner+cwd, and placeholder titles fall back to the last-message label. Subagent child rows keep their own descriptions. Locale note: sync:localization-catalog also restored parity for keys already missing on main (add-host, sleep-worktree copy).
This commit is contained in:
@@ -11,6 +11,7 @@ import { DashboardAgentRowToolStep } from './DashboardAgentRowToolStep'
|
||||
import type { AgentStatusState } from '../../../../shared/agent-status-types'
|
||||
import type { DashboardAgentRow as DashboardAgentRowData } from './useDashboardData'
|
||||
import { getAgentRowPrimaryText } from '@/lib/agent-row-primary-text'
|
||||
import { useAgentRowConversationName } from './use-agent-row-conversation-name'
|
||||
|
||||
// Why: narrow the dashboard's rollup states to shared dot states, defaulting unknowns to 'idle' so a row never crashes.
|
||||
function asDotState(state: AgentStatusState | 'idle'): AgentDotState {
|
||||
@@ -155,7 +156,8 @@ const DashboardAgentRow = React.memo(function DashboardAgentRow({
|
||||
)
|
||||
const startedAt = agent.startedAt > 0 ? agent.startedAt : null
|
||||
const doneAt = lastEnteredDoneAt(agent)
|
||||
const prompt = getAgentRowPrimaryText(agent.entry)
|
||||
const conversationName = useAgentRowConversationName(agent)
|
||||
const prompt = conversationName ?? getAgentRowPrimaryText(agent.entry)
|
||||
// Why: prompt is '' when unknown, so fall back to the state label to keep the row labeled.
|
||||
const displayLabel = prompt || agentStateLabel(asDotState(agent.state))
|
||||
const model = agent.entry.model?.trim() ?? ''
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { AppState } from '@/store/types'
|
||||
import { useAgentRowConversationName } from './use-agent-row-conversation-name'
|
||||
import type { DashboardAgentRow } from './useDashboardData'
|
||||
|
||||
const storeState = vi.hoisted(() => ({
|
||||
current: { settings: {} } as { current?: unknown; settings: Record<string, unknown> }
|
||||
}))
|
||||
|
||||
// Why: the mocked selector makes the hook a pure function, so tests can call it
|
||||
// directly without mounting a component.
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: (selector: (state: AppState) => unknown) =>
|
||||
selector(storeState.current as unknown as AppState)
|
||||
}))
|
||||
|
||||
function makeAgent(overrides: Partial<DashboardAgentRow> = {}): DashboardAgentRow {
|
||||
return {
|
||||
paneKey: 'tab-1:leaf-1',
|
||||
entry: { prompt: 'fix the sidebar' },
|
||||
tab: { customTitle: 'Patient sync spike', title: '' },
|
||||
agentType: 'claude',
|
||||
state: 'working',
|
||||
startedAt: 0,
|
||||
...overrides
|
||||
} as DashboardAgentRow
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
storeState.current = { settings: {} }
|
||||
})
|
||||
|
||||
describe('useAgentRowConversationName', () => {
|
||||
it('returns null while the setting is off', () => {
|
||||
expect(useAgentRowConversationName(makeAgent())).toBeNull()
|
||||
})
|
||||
|
||||
it('returns the conversation name when the setting is on', () => {
|
||||
storeState.current = { settings: { agentRowsUseConversationName: true } }
|
||||
expect(useAgentRowConversationName(makeAgent())).toBe('Patient sync spike')
|
||||
})
|
||||
|
||||
it('never renames subagent child rows after the parent tab', () => {
|
||||
storeState.current = { settings: { agentRowsUseConversationName: true } }
|
||||
expect(useAgentRowConversationName(makeAgent({ rowSource: 'subagent' }))).toBeNull()
|
||||
})
|
||||
|
||||
it('honors the generated-titles setting for generated names', () => {
|
||||
const agent = makeAgent({
|
||||
tab: { customTitle: null, title: '', generatedTitle: 'Fix intake flow' }
|
||||
} as Partial<DashboardAgentRow>)
|
||||
storeState.current = { settings: { agentRowsUseConversationName: true } }
|
||||
expect(useAgentRowConversationName(agent)).toBeNull()
|
||||
storeState.current = {
|
||||
settings: { agentRowsUseConversationName: true, tabAutoGenerateTitle: true }
|
||||
}
|
||||
expect(useAgentRowConversationName(agent)).toBe('Fix intake flow')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
import { getAgentRowConversationName } from '../../../../shared/agent-row-conversation-name'
|
||||
import { useAppStore } from '@/store'
|
||||
import type { DashboardAgentRow } from './useDashboardData'
|
||||
|
||||
/** The row's conversation name, or null when the mode is off or nothing usable exists. */
|
||||
export function useAgentRowConversationName(agent: DashboardAgentRow): string | null {
|
||||
const enabled = useAppStore((s) => s.settings?.agentRowsUseConversationName === true)
|
||||
const generatedTitlesEnabled = useAppStore((s) => s.settings?.tabAutoGenerateTitle === true)
|
||||
if (!enabled) {
|
||||
return null
|
||||
}
|
||||
// Why: subagent child rows describe the child, and their agentType carries the
|
||||
// child's name — the parent tab's conversation name would mislabel them.
|
||||
if (agent.rowSource === 'subagent') {
|
||||
return null
|
||||
}
|
||||
return getAgentRowConversationName(agent.tab, agent.agentType, generatedTitlesEnabled)
|
||||
}
|
||||
@@ -22,6 +22,10 @@ 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,
|
||||
@@ -833,6 +837,8 @@ export function AgentsPane({
|
||||
|
||||
<AgentGeneratedTabTitlesSetting settings={settings} updateSettings={updateSettings} />
|
||||
|
||||
<AgentRowConversationNamesSetting settings={settings} updateSettings={updateSettings} />
|
||||
|
||||
<AgentAwakeSetting settings={settings} updateSettings={updateSettings} />
|
||||
|
||||
<AgentCacheTimerSection settings={settings} updateSettings={updateSettings} />
|
||||
@@ -1003,3 +1009,25 @@ 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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
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 the conversation name (tab rename or agent-set title) on sidebar and dashboard agent rows instead of the last message. Rows without a 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,6 +9,11 @@ import {
|
||||
getAgentGeneratedTabTitlesSearchKeywords,
|
||||
getAgentGeneratedTabTitlesTitle
|
||||
} from './agent-generated-tab-title-copy'
|
||||
import {
|
||||
getAgentRowConversationNamesDescription,
|
||||
getAgentRowConversationNamesSearchKeywords,
|
||||
getAgentRowConversationNamesTitle
|
||||
} from './agent-row-conversation-name-copy'
|
||||
import {
|
||||
getAgentStatusHooksDescription,
|
||||
getAgentStatusHooksSearchKeywords,
|
||||
@@ -111,6 +116,11 @@ const getAllAgentsPaneSearchEntries = createLocalizedCatalog(() => [
|
||||
description: getAgentGeneratedTabTitlesDescription(),
|
||||
keywords: getAgentGeneratedTabTitlesSearchKeywords()
|
||||
},
|
||||
{
|
||||
title: getAgentRowConversationNamesTitle(),
|
||||
description: getAgentRowConversationNamesDescription(),
|
||||
keywords: getAgentRowConversationNamesSearchKeywords()
|
||||
},
|
||||
{
|
||||
title: getAgentAwakeTitle(),
|
||||
description: getAgentAwakeDescription(),
|
||||
|
||||
@@ -8,6 +8,7 @@ import { cn } from '@/lib/utils'
|
||||
import { getAgentDotState } from './worktree-card-agent-summary'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { getAgentRowPrimaryText } from '@/lib/agent-row-primary-text'
|
||||
import { useAgentRowConversationName } from '@/components/dashboard/use-agent-row-conversation-name'
|
||||
import { lastEnteredDoneAt } from '@/components/dashboard/agent-finished-timestamp'
|
||||
import CacheTimer, { usePromptCacheCountdownForPane } from './CacheTimer'
|
||||
|
||||
@@ -27,8 +28,11 @@ function formatShortTimeAgo(ts: number, now: number): string {
|
||||
return `${Math.floor(hours / 24)}d`
|
||||
}
|
||||
|
||||
function getCompactAgentPrimary(agent: DashboardAgentRowData): string {
|
||||
const prompt = getAgentRowPrimaryText(agent.entry)
|
||||
function getCompactAgentPrimary(
|
||||
agent: DashboardAgentRowData,
|
||||
conversationName: string | null
|
||||
): string {
|
||||
const prompt = conversationName ?? getAgentRowPrimaryText(agent.entry)
|
||||
return prompt || agentStateLabel(getAgentDotState(agent))
|
||||
}
|
||||
|
||||
@@ -116,7 +120,8 @@ export const CompactAgentRow = React.memo(function CompactAgentRow({
|
||||
// "?" glyph. Nesting under the parent already conveys identity.
|
||||
const hideIcon = hideIdentityIcon || agent.rowSource === 'subagent'
|
||||
const dotState = getAgentDotState(agent)
|
||||
const primary = getCompactAgentPrimary(agent)
|
||||
const conversationName = useAgentRowConversationName(agent)
|
||||
const primary = getCompactAgentPrimary(agent, conversationName)
|
||||
const isLineageChild = agent.lineage?.depth === 1
|
||||
const secondary = getCompactAgentSecondary(agent)
|
||||
const model = agent.entry.model?.trim() ?? ''
|
||||
|
||||
@@ -1269,7 +1269,17 @@
|
||||
"noRunTargets": "No run targets are ready for this project.",
|
||||
"perWorkspaceEnvHint": "Provision an on-demand environment from a recipe",
|
||||
"branchName": "Branch name",
|
||||
"branchNamePlaceholder": "feature/my-branch"
|
||||
"branchNamePlaceholder": "feature/my-branch",
|
||||
"connectTimedOut": "Connection timed out. It may still be connecting in the background.",
|
||||
"connectingHost": "Connecting…",
|
||||
"connectHost": "Connect",
|
||||
"addHost": "Add host",
|
||||
"addHostHint": "Register another machine or Orca server",
|
||||
"addSshHost": "Add SSH host",
|
||||
"addSshHostHint": "Use an existing machine over SSH",
|
||||
"addRemoteOrcaServer": "Add Remote Orca Server",
|
||||
"addRemoteOrcaServerHint": "Pair another Orca runtime",
|
||||
"hostConnectionFailed": "Connection failed"
|
||||
},
|
||||
"NewWorkspaceComposerModal": {
|
||||
"createWorktree": "Create worktree",
|
||||
@@ -4549,7 +4559,14 @@
|
||||
"worktree": {
|
||||
"flow": {
|
||||
"c460fecc4a": "Failed to sleep some workspaces",
|
||||
"8bc3fc0671": "Failed to sleep workspace"
|
||||
"8bc3fc0671": "Failed to sleep workspace",
|
||||
"legacy": {
|
||||
"unverified": "The older host runtime could not confirm terminal shutdown. The workspace was kept open; update the host and try again."
|
||||
},
|
||||
"host": {
|
||||
"unverified": "The host could not confirm terminal shutdown. The workspace was kept open; check the connection and try again."
|
||||
},
|
||||
"retry": "The workspace was kept open. Try again; if the problem continues, check the host connection."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4816,7 +4833,8 @@
|
||||
"sshImportSynced": "Synced {{value0}} host{{value1}}.",
|
||||
"sshImportFailed": "Failed to import SSH config.",
|
||||
"importing": "Importing...",
|
||||
"importSshConfig": "Import ~/.ssh/config"
|
||||
"importSshConfig": "Import ~/.ssh/config",
|
||||
"advanced": "Advanced"
|
||||
},
|
||||
"ForgetSshWorkspaceDialog": {
|
||||
"reconnectFailed": "Reconnection failed",
|
||||
@@ -7283,7 +7301,14 @@
|
||||
"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."
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"appearance": {
|
||||
@@ -9290,6 +9315,10 @@
|
||||
"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 the conversation name (tab rename or agent-set title) on sidebar and dashboard agent rows instead of the last message. Rows without a name keep the last message."
|
||||
}
|
||||
},
|
||||
"right": {
|
||||
|
||||
@@ -1246,7 +1246,17 @@
|
||||
"noRunTargets": "No hay destinos de ejecución listos para este proyecto.",
|
||||
"perWorkspaceEnvHint": "Provisiona un entorno bajo demanda a partir de una receta",
|
||||
"branchName": "Nombre de rama",
|
||||
"branchNamePlaceholder": "feature/my-branch"
|
||||
"branchNamePlaceholder": "feature/my-branch",
|
||||
"connectTimedOut": "Connection timed out. It may still be connecting in the background.",
|
||||
"connectingHost": "Connecting…",
|
||||
"connectHost": "Connect",
|
||||
"addHost": "Add host",
|
||||
"addHostHint": "Register another machine or Orca server",
|
||||
"addSshHost": "Add SSH host",
|
||||
"addSshHostHint": "Use an existing machine over SSH",
|
||||
"addRemoteOrcaServer": "Add Remote Orca Server",
|
||||
"addRemoteOrcaServerHint": "Pair another Orca runtime",
|
||||
"hostConnectionFailed": "Connection failed"
|
||||
},
|
||||
"NewWorkspaceComposerModal": {
|
||||
"createWorktree": "Crear worktree",
|
||||
@@ -4503,7 +4513,14 @@
|
||||
"worktree": {
|
||||
"flow": {
|
||||
"c460fecc4a": "No se pudieron poner en reposo algunos espacios de trabajo",
|
||||
"8bc3fc0671": "No se pudo poner en reposo el espacio de trabajo"
|
||||
"8bc3fc0671": "No se pudo poner en reposo el espacio de trabajo",
|
||||
"legacy": {
|
||||
"unverified": "The older host runtime could not confirm terminal shutdown. The workspace was kept open; update the host and try again."
|
||||
},
|
||||
"host": {
|
||||
"unverified": "The host could not confirm terminal shutdown. The workspace was kept open; check the connection and try again."
|
||||
},
|
||||
"retry": "The workspace was kept open. Try again; if the problem continues, check the host connection."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4793,7 +4810,8 @@
|
||||
"sshImportFailed": "No se pudo importar la configuración SSH.",
|
||||
"importing": "Importando...",
|
||||
"importSshConfig": "Importar ~/.ssh/config",
|
||||
"sshPersistenceDefault": "Los terminales remotos en este host seguirán activos hasta que los cierres o restablezcas el relay."
|
||||
"sshPersistenceDefault": "Los terminales remotos en este host seguirán activos hasta que los cierres o restablezcas el relay.",
|
||||
"advanced": "Advanced"
|
||||
},
|
||||
"ForgetSshWorkspaceDialog": {
|
||||
"reconnectFailed": "Error de reconexión",
|
||||
@@ -7223,7 +7241,14 @@
|
||||
"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."
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"appearance": {
|
||||
@@ -9267,6 +9292,10 @@
|
||||
"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 the conversation name (tab rename or agent-set title) on sidebar and dashboard agent rows instead of the last message. Rows without a name keep the last message."
|
||||
}
|
||||
},
|
||||
"right": {
|
||||
|
||||
@@ -1246,7 +1246,17 @@
|
||||
"noRunTargets": "このプロジェクトには実行ターゲットが準備されていません。",
|
||||
"perWorkspaceEnvHint": "レシピからオンデマンド環境をプロビジョニングする",
|
||||
"branchName": "ブランチ名",
|
||||
"branchNamePlaceholder": "feature/my-branch"
|
||||
"branchNamePlaceholder": "feature/my-branch",
|
||||
"connectTimedOut": "Connection timed out. It may still be connecting in the background.",
|
||||
"connectingHost": "Connecting…",
|
||||
"connectHost": "Connect",
|
||||
"addHost": "Add host",
|
||||
"addHostHint": "Register another machine or Orca server",
|
||||
"addSshHost": "Add SSH host",
|
||||
"addSshHostHint": "Use an existing machine over SSH",
|
||||
"addRemoteOrcaServer": "Add Remote Orca Server",
|
||||
"addRemoteOrcaServerHint": "Pair another Orca runtime",
|
||||
"hostConnectionFailed": "Connection failed"
|
||||
},
|
||||
"NewWorkspaceComposerModal": {
|
||||
"createWorktree": "ワークツリーを作成する",
|
||||
@@ -4484,7 +4494,14 @@
|
||||
"worktree": {
|
||||
"flow": {
|
||||
"c460fecc4a": "一部のワークスペースのスリープに失敗しました",
|
||||
"8bc3fc0671": "ワークスペースのスリープに失敗しました"
|
||||
"8bc3fc0671": "ワークスペースのスリープに失敗しました",
|
||||
"legacy": {
|
||||
"unverified": "The older host runtime could not confirm terminal shutdown. The workspace was kept open; update the host and try again."
|
||||
},
|
||||
"host": {
|
||||
"unverified": "The host could not confirm terminal shutdown. The workspace was kept open; check the connection and try again."
|
||||
},
|
||||
"retry": "The workspace was kept open. Try again; if the problem continues, check the host connection."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4793,7 +4810,8 @@
|
||||
"sshImportFailed": "SSH 構成のインポートに失敗しました。",
|
||||
"importing": "インポート中...",
|
||||
"importSshConfig": "~/.ssh/config をインポートします",
|
||||
"sshPersistenceDefault": "このホスト上のリモートターミナルは、終了するかリレーをリセットするまで動作し続けます。"
|
||||
"sshPersistenceDefault": "このホスト上のリモートターミナルは、終了するかリレーをリセットするまで動作し続けます。",
|
||||
"advanced": "Advanced"
|
||||
},
|
||||
"ForgetSshWorkspaceDialog": {
|
||||
"reconnectFailed": "再接続に失敗しました",
|
||||
@@ -7245,7 +7263,14 @@
|
||||
"agentPermissions": "Agent Permissions",
|
||||
"agentPermissionsDescription": "Switch agent permission defaults between Yolo and Manual.",
|
||||
"agentRuntime": "Agent のランタイム",
|
||||
"agentRuntimeDescription": "デフォルトで agents を Windows または WSL のどちらで検出および起動するかを選択します。"
|
||||
"agentRuntimeDescription": "デフォルトで agents を Windows または WSL のどちらで検出および起動するかを選択します。",
|
||||
"conversation": "conversation",
|
||||
"conversationName": "conversation name",
|
||||
"sidebar": "sidebar",
|
||||
"dashboard": "dashboard",
|
||||
"activityRow": "activity",
|
||||
"lastMessage": "last message",
|
||||
"rowLabel": "label"
|
||||
}
|
||||
},
|
||||
"appearance": {
|
||||
@@ -9267,6 +9292,10 @@
|
||||
"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 the conversation name (tab rename or agent-set title) on sidebar and dashboard agent rows instead of the last message. Rows without a name keep the last message."
|
||||
}
|
||||
},
|
||||
"right": {
|
||||
|
||||
@@ -1246,7 +1246,17 @@
|
||||
"noRunTargets": "이 프로젝트에는 실행 대상이 준비되어 있지 않습니다.",
|
||||
"perWorkspaceEnvHint": "레시피에서 온디맨드 환경 프로비저닝",
|
||||
"branchName": "브랜치 이름",
|
||||
"branchNamePlaceholder": "feature/my-branch"
|
||||
"branchNamePlaceholder": "feature/my-branch",
|
||||
"connectTimedOut": "Connection timed out. It may still be connecting in the background.",
|
||||
"connectingHost": "Connecting…",
|
||||
"connectHost": "Connect",
|
||||
"addHost": "Add host",
|
||||
"addHostHint": "Register another machine or Orca server",
|
||||
"addSshHost": "Add SSH host",
|
||||
"addSshHostHint": "Use an existing machine over SSH",
|
||||
"addRemoteOrcaServer": "Add Remote Orca Server",
|
||||
"addRemoteOrcaServerHint": "Pair another Orca runtime",
|
||||
"hostConnectionFailed": "Connection failed"
|
||||
},
|
||||
"NewWorkspaceComposerModal": {
|
||||
"createWorktree": "작업 트리 만들기",
|
||||
@@ -4484,7 +4494,14 @@
|
||||
"worktree": {
|
||||
"flow": {
|
||||
"c460fecc4a": "일부 워크스페이스를 절전 모드로 전환하지 못했습니다.",
|
||||
"8bc3fc0671": "워크스페이스를 절전 모드로 전환하지 못했습니다."
|
||||
"8bc3fc0671": "워크스페이스를 절전 모드로 전환하지 못했습니다.",
|
||||
"legacy": {
|
||||
"unverified": "The older host runtime could not confirm terminal shutdown. The workspace was kept open; update the host and try again."
|
||||
},
|
||||
"host": {
|
||||
"unverified": "The host could not confirm terminal shutdown. The workspace was kept open; check the connection and try again."
|
||||
},
|
||||
"retry": "The workspace was kept open. Try again; if the problem continues, check the host connection."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4793,7 +4810,8 @@
|
||||
"sshImportFailed": "SSH 구성을 가져오지 못했습니다.",
|
||||
"importing": "가져오는 중...",
|
||||
"importSshConfig": "~/.ssh/config 가져오기",
|
||||
"sshPersistenceDefault": "이 호스트의 원격 터미널은 종료하거나 릴레이를 재설정할 때까지 계속 실행됩니다."
|
||||
"sshPersistenceDefault": "이 호스트의 원격 터미널은 종료하거나 릴레이를 재설정할 때까지 계속 실행됩니다.",
|
||||
"advanced": "Advanced"
|
||||
},
|
||||
"ForgetSshWorkspaceDialog": {
|
||||
"reconnectFailed": "재연결 실패",
|
||||
@@ -7208,7 +7226,14 @@
|
||||
"agentPermissions": "Agent 권한",
|
||||
"agentPermissionsDescription": "agent 권한 기본값을 Yolo와 수동 사이에서 전환합니다.",
|
||||
"agentRuntime": "Agent 런타임",
|
||||
"agentRuntimeDescription": "기본적으로 Windows 또는 WSL에서 agents를 감지하고 시작할지 선택합니다."
|
||||
"agentRuntimeDescription": "기본적으로 Windows 또는 WSL에서 agents를 감지하고 시작할지 선택합니다.",
|
||||
"conversation": "conversation",
|
||||
"conversationName": "conversation name",
|
||||
"sidebar": "sidebar",
|
||||
"dashboard": "dashboard",
|
||||
"activityRow": "activity",
|
||||
"lastMessage": "last message",
|
||||
"rowLabel": "label"
|
||||
}
|
||||
},
|
||||
"appearance": {
|
||||
@@ -9267,6 +9292,10 @@
|
||||
"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 the conversation name (tab rename or agent-set title) on sidebar and dashboard agent rows instead of the last message. Rows without a name keep the last message."
|
||||
}
|
||||
},
|
||||
"right": {
|
||||
|
||||
@@ -1246,7 +1246,17 @@
|
||||
"noRunTargets": "此项目还没有可用的运行目标。",
|
||||
"perWorkspaceEnvHint": "通过环境模板按需创建环境",
|
||||
"branchName": "分支名称",
|
||||
"branchNamePlaceholder": "feature/my-branch"
|
||||
"branchNamePlaceholder": "feature/my-branch",
|
||||
"connectTimedOut": "Connection timed out. It may still be connecting in the background.",
|
||||
"connectingHost": "Connecting…",
|
||||
"connectHost": "Connect",
|
||||
"addHost": "Add host",
|
||||
"addHostHint": "Register another machine or Orca server",
|
||||
"addSshHost": "Add SSH host",
|
||||
"addSshHostHint": "Use an existing machine over SSH",
|
||||
"addRemoteOrcaServer": "Add Remote Orca Server",
|
||||
"addRemoteOrcaServerHint": "Pair another Orca runtime",
|
||||
"hostConnectionFailed": "Connection failed"
|
||||
},
|
||||
"NewWorkspaceComposerModal": {
|
||||
"createWorktree": "创建工作树",
|
||||
@@ -4484,7 +4494,14 @@
|
||||
"worktree": {
|
||||
"flow": {
|
||||
"c460fecc4a": "无法睡眠某些工作区",
|
||||
"8bc3fc0671": "无法睡眠工作区"
|
||||
"8bc3fc0671": "无法睡眠工作区",
|
||||
"legacy": {
|
||||
"unverified": "The older host runtime could not confirm terminal shutdown. The workspace was kept open; update the host and try again."
|
||||
},
|
||||
"host": {
|
||||
"unverified": "The host could not confirm terminal shutdown. The workspace was kept open; check the connection and try again."
|
||||
},
|
||||
"retry": "The workspace was kept open. Try again; if the problem continues, check the host connection."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4793,7 +4810,8 @@
|
||||
"sshImportFailed": "导入 SSH 配置失败。",
|
||||
"importing": "正在导入...",
|
||||
"importSshConfig": "导入 ~/.ssh/config",
|
||||
"sshPersistenceDefault": "此主机上的远程终端会保持运行,直到你结束它们或重置中继。"
|
||||
"sshPersistenceDefault": "此主机上的远程终端会保持运行,直到你结束它们或重置中继。",
|
||||
"advanced": "Advanced"
|
||||
},
|
||||
"ForgetSshWorkspaceDialog": {
|
||||
"reconnectFailed": "重新连接失败",
|
||||
@@ -7208,7 +7226,14 @@
|
||||
"agentPermissions": "智能体权限",
|
||||
"agentPermissionsDescription": "在 Yolo 和手动之间切换智能体权限默认值。",
|
||||
"agentRuntime": "智能体运行时",
|
||||
"agentRuntimeDescription": "选择默认在 Windows 还是 WSL 中检测并启动智能体。"
|
||||
"agentRuntimeDescription": "选择默认在 Windows 还是 WSL 中检测并启动智能体。",
|
||||
"conversation": "conversation",
|
||||
"conversationName": "conversation name",
|
||||
"sidebar": "sidebar",
|
||||
"dashboard": "dashboard",
|
||||
"activityRow": "activity",
|
||||
"lastMessage": "last message",
|
||||
"rowLabel": "label"
|
||||
}
|
||||
},
|
||||
"appearance": {
|
||||
@@ -9267,6 +9292,10 @@
|
||||
"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 the conversation name (tab rename or agent-set title) on sidebar and dashboard agent rows instead of the last message. Rows without a name keep the last message."
|
||||
}
|
||||
},
|
||||
"right": {
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
getAgentRowConversationName,
|
||||
type ConversationNameTab
|
||||
} from './agent-row-conversation-name'
|
||||
|
||||
function makeTab(overrides: Partial<ConversationNameTab> = {}): ConversationNameTab {
|
||||
return { customTitle: null, title: '', ...overrides }
|
||||
}
|
||||
|
||||
describe('getAgentRowConversationName', () => {
|
||||
it('prefers the manual tab rename over every other source', () => {
|
||||
const tab = makeTab({
|
||||
customTitle: 'Patient sync spike',
|
||||
quickCommandLabel: 'Run tests',
|
||||
generatedTitle: 'Fix intake flow',
|
||||
title: '✳ Investigate replay bug'
|
||||
})
|
||||
expect(getAgentRowConversationName(tab, 'claude', true)).toBe('Patient sync spike')
|
||||
})
|
||||
|
||||
it('falls back to the quick-command label before titles', () => {
|
||||
const tab = makeTab({ quickCommandLabel: 'Run tests', title: '✳ Investigate replay bug' })
|
||||
expect(getAgentRowConversationName(tab, 'claude', true)).toBe('Run tests')
|
||||
})
|
||||
|
||||
it('keeps OpenCode semantic session titles whole', () => {
|
||||
const tab = makeTab({ title: 'OC | build the release pipeline' })
|
||||
expect(getAgentRowConversationName(tab, 'opencode', false)).toBe(
|
||||
'OC | build the release pipeline'
|
||||
)
|
||||
})
|
||||
|
||||
it('uses the generated title only when generated titles are enabled', () => {
|
||||
const tab = makeTab({ generatedTitle: 'Fix intake flow', title: '✳ Investigate replay bug' })
|
||||
expect(getAgentRowConversationName(tab, 'claude', true)).toBe('Fix intake flow')
|
||||
expect(getAgentRowConversationName(tab, 'claude', false)).toBe('Investigate replay bug')
|
||||
})
|
||||
|
||||
it('strips leading status decoration from agent-set titles', () => {
|
||||
expect(
|
||||
getAgentRowConversationName(makeTab({ title: '✳ Fix patient intake flow' }), 'claude', false)
|
||||
).toBe('Fix patient intake flow')
|
||||
expect(
|
||||
getAgentRowConversationName(makeTab({ title: '⠋ Refactor replay guard' }), 'codex', false)
|
||||
).toBe('Refactor replay guard')
|
||||
})
|
||||
|
||||
it('rejects spinner+cwd titles instead of surfacing paths as names', () => {
|
||||
expect(
|
||||
getAgentRowConversationName(makeTab({ title: '⠋ ~/orca/workspaces' }), 'codex', false)
|
||||
).toBeNull()
|
||||
expect(
|
||||
getAgentRowConversationName(makeTab({ title: '/Users/dev/repo' }), 'codex', false)
|
||||
).toBeNull()
|
||||
expect(
|
||||
getAgentRowConversationName(makeTab({ title: 'C:\\repos\\orca' }), 'codex', false)
|
||||
).toBeNull()
|
||||
expect(
|
||||
getAgentRowConversationName(makeTab({ title: 'orca/workspaces' }), 'codex', false)
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('accepts multi-word titles that merely contain a slash', () => {
|
||||
expect(
|
||||
getAgentRowConversationName(makeTab({ title: 'Fix a/b toggle in settings' }), 'codex', false)
|
||||
).toBe('Fix a/b toggle in settings')
|
||||
})
|
||||
|
||||
it('rejects synthetic status titles', () => {
|
||||
expect(
|
||||
getAgentRowConversationName(makeTab({ title: 'Codex ready' }), 'codex', false)
|
||||
).toBeNull()
|
||||
expect(
|
||||
getAgentRowConversationName(makeTab({ title: 'Codex - action required' }), 'codex', false)
|
||||
).toBeNull()
|
||||
expect(
|
||||
getAgentRowConversationName(makeTab({ title: 'Cursor Agent' }), 'cursor', false)
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('rejects identity-echo, management, and placeholder titles', () => {
|
||||
expect(getAgentRowConversationName(makeTab({ title: 'Claude' }), 'claude', false)).toBeNull()
|
||||
expect(
|
||||
getAgentRowConversationName(makeTab({ title: 'claude agents' }), 'claude', false)
|
||||
).toBeNull()
|
||||
expect(getAgentRowConversationName(makeTab({ title: 'Agent' }), 'claude', false)).toBeNull()
|
||||
})
|
||||
|
||||
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 2', defaultTitle: 'Terminal 2' }),
|
||||
'claude',
|
||||
false
|
||||
)
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,103 @@
|
||||
// Resolves the stable "conversation name" an agent row can show instead of the
|
||||
// live last-message preview. Sources, in the same precedence the tab bar uses
|
||||
// (tab-title-resolution.ts): manual rename → quick-command label → OpenCode's
|
||||
// semantic session title → Orca's generated title → the agent-set live title.
|
||||
// Live titles are accepted only when they carry a real name — pure status,
|
||||
// identity-echo, and spinner/cwd titles yield null so callers keep the
|
||||
// last-message label.
|
||||
import type { AgentType } from './agent-status-types'
|
||||
import { isClaudeManagementTitle } from './agent-title-core'
|
||||
import { stripLeadingAgentTitleDecorationOrEmpty } from './agent-title-decoration'
|
||||
import { formatAgentTypeLabel } from './agent-type-label'
|
||||
import { isMeaningfulOpenCodeTerminalTitle } from './opencode-terminal-title'
|
||||
import { SYNTHETIC_AGENT_TITLE_PROFILES } from './synthetic-agent-title'
|
||||
import type { TerminalTab } from './types'
|
||||
|
||||
export type ConversationNameTab = Pick<
|
||||
TerminalTab,
|
||||
'customTitle' | 'quickCommandLabel' | 'generatedTitle' | 'title' | 'defaultTitle'
|
||||
>
|
||||
|
||||
// Why: synthetic status titles ("Codex ready", "Cursor - action required") are
|
||||
// state, not names. Precomputed once; the profile table is a module constant.
|
||||
const SYNTHETIC_STATUS_TITLES_LOWER: ReadonlySet<string> = new Set(
|
||||
Object.values(SYNTHETIC_AGENT_TITLE_PROFILES).flatMap((profile) => [
|
||||
profile.workingLabel.toLowerCase(),
|
||||
profile.permissionLabel.toLowerCase(),
|
||||
profile.idleLabel.toLowerCase()
|
||||
])
|
||||
)
|
||||
|
||||
// Why: retained rows without a live tab synthesize `title: 'Agent'`
|
||||
// (worktree-agent-row-fallback-tab.ts); it is a placeholder, not a name.
|
||||
const FALLBACK_TAB_TITLE_LOWER = 'agent'
|
||||
|
||||
function isCwdLikeTitle(title: string): boolean {
|
||||
// Hook-less agents over SSH surface spinner+cwd titles (#8711); once the
|
||||
// spinner is stripped, what remains is a path, not a conversation name.
|
||||
if (/^(?:~|\/|[A-Za-z]:[\\/])/.test(title)) {
|
||||
return true
|
||||
}
|
||||
// A single path-ish token ("orca/workspaces") is still a cwd, not a name.
|
||||
return !/\s/.test(title) && title.includes('/')
|
||||
}
|
||||
|
||||
function conversationNameFromLiveTitle(
|
||||
liveTitle: string,
|
||||
agentTypeLabelLower: string,
|
||||
defaultTitle: string | undefined
|
||||
): string | null {
|
||||
const stripped = stripLeadingAgentTitleDecorationOrEmpty(liveTitle.trim()).trim()
|
||||
if (!stripped) {
|
||||
return null
|
||||
}
|
||||
const lower = stripped.toLowerCase()
|
||||
if (
|
||||
SYNTHETIC_STATUS_TITLES_LOWER.has(lower) ||
|
||||
lower === FALLBACK_TAB_TITLE_LOWER ||
|
||||
lower === agentTypeLabelLower ||
|
||||
isClaudeManagementTitle(stripped) ||
|
||||
isCwdLikeTitle(stripped)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
if (defaultTitle && stripped === defaultTitle.trim()) {
|
||||
return null
|
||||
}
|
||||
return stripped
|
||||
}
|
||||
|
||||
/**
|
||||
* The conversation name for an agent row, or null when no usable name exists
|
||||
* and the caller should keep its last-message label.
|
||||
*/
|
||||
export function getAgentRowConversationName(
|
||||
tab: ConversationNameTab,
|
||||
agentType: AgentType | null | undefined,
|
||||
generatedTitlesEnabled: boolean
|
||||
): string | null {
|
||||
const customTitle = tab.customTitle?.trim()
|
||||
if (customTitle) {
|
||||
return customTitle
|
||||
}
|
||||
const quickCommandLabel = tab.quickCommandLabel?.trim()
|
||||
if (quickCommandLabel) {
|
||||
return quickCommandLabel
|
||||
}
|
||||
const liveTitle = tab.title?.trim() ?? ''
|
||||
if (isMeaningfulOpenCodeTerminalTitle(liveTitle)) {
|
||||
return liveTitle
|
||||
}
|
||||
const generatedTitle = generatedTitlesEnabled ? tab.generatedTitle?.trim() : ''
|
||||
if (generatedTitle) {
|
||||
return generatedTitle
|
||||
}
|
||||
if (!liveTitle) {
|
||||
return null
|
||||
}
|
||||
return conversationNameFromLiveTitle(
|
||||
liveTitle,
|
||||
formatAgentTypeLabel(agentType).toLowerCase(),
|
||||
tab.defaultTitle
|
||||
)
|
||||
}
|
||||
@@ -321,6 +321,7 @@ 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/*.
|
||||
|
||||
@@ -2868,6 +2868,10 @@ 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. */
|
||||
|
||||
Reference in New Issue
Block a user