diff --git a/shared/chat/session-title.ts b/shared/chat/session-title.ts index d57f6378..93a7c73c 100644 --- a/shared/chat/session-title.ts +++ b/shared/chat/session-title.ts @@ -2,6 +2,14 @@ const ACP_WORKING_DIRECTORY_PREFIX = /^\[Working directory: [^\r\n]*\](?:\r?\n){ const ACP_WORKING_DIRECTORY_TRUNCATED_TITLE = /^\[Working directory: [^\r\n]*\]…$/ const OPENCLAW_SESSION_ID_FALLBACK_TITLE = /^([0-9a-f]{8}) \((\d{4}-\d{2}-\d{2})\)$/i +export type SessionTitleSource = { + key: string + sessionId?: string + label?: string + derivedTitle?: string + displayName?: string +} + export function stripAcpWorkingDirectoryPrefix(text: string): string { return text.replace(ACP_WORKING_DIRECTORY_PREFIX, '') } @@ -19,3 +27,20 @@ export function isOpenClawSessionIdFallbackTitle( const match = text.trim().match(OPENCLAW_SESSION_ID_FALLBACK_TITLE) return Boolean(match && normalizedSessionId.startsWith(match[1]!.toLowerCase())) } + +/** Resolve the same human-readable title used for a session everywhere in the UI. */ +export function getSessionDisplayTitle( + session: SessionTitleSource, + sessionLabels: Record = {}, +): string { + const candidates = [ + sessionLabels[session.key], + session.label, + session.derivedTitle, + session.displayName, + ] + return candidates.find((candidate) => ( + candidate?.trim() + && !isOpenClawSessionIdFallbackTitle(candidate, session.sessionId) + ))?.trim() ?? session.key +} diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 3b2d8abe..c8c2bae2 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -34,7 +34,7 @@ import { cn } from '@/lib/utils'; import { isGatewayRestarting } from '@/lib/gateway-status'; import { rendererExtensionRegistry } from '@/extensions/registry'; import { useSettingsStore } from '@/stores/settings'; -import { useChatStore, type ChatSession } from '@/stores/chat'; +import { useChatStore } from '@/stores/chat'; import { useSessionAttentionStore } from '@/stores/session-attention'; import { useGatewayStore } from '@/stores/gateway'; import { useAgentsStore } from '@/stores/agents'; @@ -54,7 +54,7 @@ import { useNewChatAction } from './use-new-chat-action'; import { isDefaultWorkspacePath } from '@/lib/workspace-context'; import { useWorkspaceAvailability } from '@/hooks/use-workspace-availability'; import { projectSessionRunState } from '@/stores/chat/session-status'; -import { isOpenClawSessionIdFallbackTitle } from '@shared/chat/session-title'; +import { getSessionDisplayTitle } from '@shared/chat/session-title'; interface NavItemProps { to: string; @@ -202,19 +202,6 @@ export function Sidebar() { const navigate = useNavigate(); const isOnChat = useLocation().pathname === '/'; - const getSessionLabel = (session: ChatSession) => { - const candidates = [ - sessionLabels[session.key], - session.label, - session.derivedTitle, - session.displayName, - ]; - return candidates.find((candidate) => ( - candidate?.trim() - && !isOpenClawSessionIdFallbackTitle(candidate, session.sessionId) - ))?.trim() ?? session.key; - }; - const openControlUi = async (view?: 'dreams', label = 'OpenClaw Page') => { try { const result = await hostApi.gateway.controlUi(view); @@ -726,7 +713,7 @@ export function Sidebar() { const agentName = agentNameById[agentId] || agentId; const isEditing = editingSessionKey === s.key; const isCurrentSession = isOnChat && currentSessionKey === s.key; - const sessionLabel = getSessionLabel(s); + const sessionLabel = getSessionDisplayTitle(s, sessionLabels); const relativeTime = formatSessionRelativeTime(activityMs, nowMs, i18n.language); const runState = projectSessionRunState(s); const attention = sessionAttentionByKey[s.key]; diff --git a/src/pages/Chat/AcpMessageSegment.tsx b/src/pages/Chat/AcpMessageSegment.tsx index 7143ea9c..e81a2789 100644 --- a/src/pages/Chat/AcpMessageSegment.tsx +++ b/src/pages/Chat/AcpMessageSegment.tsx @@ -126,7 +126,7 @@ export function AcpAssistantHoverBar({ text }: { text: string }) { const label = copied ? t('acp.copied') : t('acp.copy'); return ( -
+