diff --git a/apps/desktop/src/app/chat/sidebar/session-row.tsx b/apps/desktop/src/app/chat/sidebar/session-row.tsx index 0c2ed62d235..16d5baa8a4c 100644 --- a/apps/desktop/src/app/chat/sidebar/session-row.tsx +++ b/apps/desktop/src/app/chat/sidebar/session-row.tsx @@ -2,12 +2,14 @@ import { useStore } from '@nanostores/react' import type * as React from 'react' import { writeSessionDrag } from '@/app/chat/composer/inline-refs' +import { PlatformAvatar } from '@/app/messaging/platform-icon' import { Button } from '@/components/ui/button' import { Codicon } from '@/components/ui/codicon' import type { SessionInfo } from '@/hermes' import { type Translations, useI18n } from '@/i18n' import { sessionTitle } from '@/lib/chat-runtime' import { triggerHaptic } from '@/lib/haptics' +import { normalizeSessionSource, sessionSourceLabel } from '@/lib/session-source' import { cn } from '@/lib/utils' import { $attentionSessionIds } from '@/store/session' @@ -66,6 +68,9 @@ export function SidebarSessionRow({ const r = t.sidebar.row const title = sessionTitle(session) const age = formatAge(session.last_active || session.started_at, r) + const sourceId = normalizeSessionSource(session.source) + const sourceLabel = sessionSourceLabel(sourceId) + const showSource = Boolean(sourceId && sourceLabel && !['desktop', 'local', 'tui'].includes(sourceId)) const handleLabel = `Reorder ${title}` // Subscribe per-row (the leaf) instead of drilling a set through the list — // the atom is tiny and rarely non-empty. True when a clarify prompt in this @@ -176,12 +181,25 @@ export function SidebarSessionRow({ needsInput ? 'overflow-visible' : 'overflow-hidden' )} > - - + + )} {title} + {showSource && sourceId && sourceLabel && ( + + + {sourceLabel} + + )}
{!isWorking && ( diff --git a/apps/desktop/src/app/messaging/platform-icon.tsx b/apps/desktop/src/app/messaging/platform-icon.tsx index 6a0b32a7a81..4a6be4354db 100644 --- a/apps/desktop/src/app/messaging/platform-icon.tsx +++ b/apps/desktop/src/app/messaging/platform-icon.tsx @@ -28,15 +28,17 @@ import { cn } from '@/lib/utils' type IconKind = 'brand' | 'generic' interface PlatformIconSpec { - Icon: ComponentType> + Icon?: ComponentType> color: string kind: IconKind + monogram?: string } const PLATFORM_ICONS: Record = { telegram: { Icon: SiTelegram, color: '#26A5E4', kind: 'brand' }, discord: { Icon: SiDiscord, color: '#5865F2', kind: 'brand' }, // Slack removed from Simple Icons by Salesforce request — letter monogram. + slack: { color: '#4A154B', kind: 'brand', monogram: 'S' }, mattermost: { Icon: SiMattermost, color: '#0058CC', kind: 'brand' }, matrix: { Icon: SiMatrix, color: '#000000', kind: 'brand' }, signal: { Icon: SiSignal, color: '#3A76F0', kind: 'brand' }, @@ -87,7 +89,7 @@ export function PlatformAvatar({ className, platformId, platformName }: Platform color }} > - + {Icon ? : spec.monogram || platformName.charAt(0).toUpperCase()} ) } diff --git a/apps/desktop/src/lib/session-search.test.ts b/apps/desktop/src/lib/session-search.test.ts index aa40fe59c0c..00027ff3186 100644 --- a/apps/desktop/src/lib/session-search.test.ts +++ b/apps/desktop/src/lib/session-search.test.ts @@ -52,6 +52,14 @@ describe('sessionMatchesSearch', () => { expect(sessionMatchesSearch(session, 'hermes-agent')).toBe(true) }) + it('matches sessions by source platform and aliases', () => { + expect(sessionMatchesSearch(makeSession({ source: 'telegram' }), 'Telegram')).toBe(true) + expect(sessionMatchesSearch(makeSession({ source: 'whatsapp' }), 'WhatsApp')).toBe(true) + expect(sessionMatchesSearch(makeSession({ source: 'whatsapp' }), 'wa')).toBe(true) + expect(sessionMatchesSearch(makeSession({ source: 'slack' }), 'slack')).toBe(true) + expect(sessionMatchesSearch(makeSession({ source: 'bluebubbles' }), 'imessage')).toBe(true) + }) + it('does not match unrelated queries', () => { expect(sessionMatchesSearch(makeSession(), 'totally-unrelated')).toBe(false) }) diff --git a/apps/desktop/src/lib/session-search.ts b/apps/desktop/src/lib/session-search.ts index b8ee6ebf30c..6ec6dde85e4 100644 --- a/apps/desktop/src/lib/session-search.ts +++ b/apps/desktop/src/lib/session-search.ts @@ -1,6 +1,7 @@ import type { SessionInfo } from '@/types/hermes' import { sessionTitle } from './chat-runtime' +import { sessionSourceSearchTerms } from './session-source' export function sessionMatchesSearch(session: SessionInfo, query: string): boolean { const needle = query.trim().toLowerCase() @@ -14,6 +15,7 @@ export function sessionMatchesSearch(session: SessionInfo, query: string): boole session._lineage_root_id ?? '', sessionTitle(session), session.preview ?? '', - session.cwd ?? '' + session.cwd ?? '', + ...sessionSourceSearchTerms(session.source) ].some(value => value.toLowerCase().includes(needle)) } diff --git a/apps/desktop/src/lib/session-source.ts b/apps/desktop/src/lib/session-source.ts new file mode 100644 index 00000000000..8940999985f --- /dev/null +++ b/apps/desktop/src/lib/session-source.ts @@ -0,0 +1,62 @@ +const SOURCE_LABELS: Record = { + api_server: 'API', + bluebubbles: 'iMessage', + cli: 'CLI', + codex: 'Codex', + desktop: 'Desktop', + discord: 'Discord', + email: 'Email', + gateway: 'Gateway', + local: 'Local', + matrix: 'Matrix', + mattermost: 'Mattermost', + qqbot: 'QQ', + signal: 'Signal', + slack: 'Slack', + sms: 'SMS', + telegram: 'Telegram', + tui: 'TUI', + webhook: 'Webhook', + weixin: 'WeChat', + whatsapp: 'WhatsApp', + yuanbao: 'Yuanbao' +} + +const SOURCE_ALIASES: Record = { + bluebubbles: ['apple messages', 'imessage'], + cli: ['terminal'], + desktop: ['app', 'gui'], + local: ['machine'], + qqbot: ['qq'], + telegram: ['tg'], + tui: ['terminal'], + weixin: ['wechat'], + whatsapp: ['wa'] +} + +export function normalizeSessionSource(source: null | string | undefined): string | null { + const id = source?.trim().toLowerCase() + + return id || null +} + +export function sessionSourceLabel(source: null | string | undefined): string | null { + const id = normalizeSessionSource(source) + + if (!id) { + return null + } + + return SOURCE_LABELS[id] || id.replace(/[_-]+/g, ' ').replace(/\b\w/g, char => char.toUpperCase()) +} + +export function sessionSourceSearchTerms(source: null | string | undefined): string[] { + const id = normalizeSessionSource(source) + const label = sessionSourceLabel(id) + + if (!id) { + return [] + } + + return [id, label ?? '', ...(SOURCE_ALIASES[id] ?? [])].filter(Boolean) +}