feat: human-friendly session titles in header and sidebar
Replace raw session keys/UUIDs with readable names derived from session metadata (label, kind, channel). Priority: label > kind-based name (Main, Cron, Task) with channel suffix > cleaned key fallback. New utility: src/lib/sessionName.ts used by both Header and Sidebar.
This commit is contained in:
@@ -2,6 +2,7 @@ import { Menu, Sparkles, LogOut, Volume2, VolumeOff, Cpu } from 'lucide-react';
|
|||||||
import type { ConnectionStatus, Session } from '../types';
|
import type { ConnectionStatus, Session } from '../types';
|
||||||
import { useT } from '../hooks/useLocale';
|
import { useT } from '../hooks/useLocale';
|
||||||
import { LanguageSelector } from './LanguageSelector';
|
import { LanguageSelector } from './LanguageSelector';
|
||||||
|
import { sessionDisplayName } from '../lib/sessionName';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
status: ConnectionStatus;
|
status: ConnectionStatus;
|
||||||
@@ -15,7 +16,7 @@ interface Props {
|
|||||||
|
|
||||||
export function Header({ status, sessionKey, onToggleSidebar, activeSessionData, onLogout, soundEnabled, onToggleSound }: Props) {
|
export function Header({ status, sessionKey, onToggleSidebar, activeSessionData, onLogout, soundEnabled, onToggleSound }: Props) {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const sessionLabel = sessionKey.split(':').pop() || sessionKey;
|
const sessionLabel = activeSessionData ? sessionDisplayName(activeSessionData) : (sessionKey.split(':').pop() || sessionKey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { X, Sparkles, Search, Pin, Trash2 } from 'lucide-react';
|
|||||||
import type { Session } from '../types';
|
import type { Session } from '../types';
|
||||||
import { useT } from '../hooks/useLocale';
|
import { useT } from '../hooks/useLocale';
|
||||||
import { SessionIcon } from './SessionIcon';
|
import { SessionIcon } from './SessionIcon';
|
||||||
|
import { sessionDisplayName } from '../lib/sessionName';
|
||||||
|
|
||||||
const PINNED_KEY = 'pinchchat-pinned-sessions';
|
const PINNED_KEY = 'pinchchat-pinned-sessions';
|
||||||
const WIDTH_KEY = 'pinchchat-sidebar-width';
|
const WIDTH_KEY = 'pinchchat-sidebar-width';
|
||||||
@@ -125,7 +126,7 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onC
|
|||||||
let list = sessions;
|
let list = sessions;
|
||||||
if (filter.trim()) {
|
if (filter.trim()) {
|
||||||
const q = filter.toLowerCase();
|
const q = filter.toLowerCase();
|
||||||
list = sessions.filter(s => (s.label || s.key).toLowerCase().includes(q));
|
list = sessions.filter(s => sessionDisplayName(s).toLowerCase().includes(q));
|
||||||
}
|
}
|
||||||
// Sort pinned sessions to top (preserving relative order within each group)
|
// Sort pinned sessions to top (preserving relative order within each group)
|
||||||
const pinnedList = list.filter(s => pinned.has(s.key));
|
const pinnedList = list.filter(s => pinned.has(s.key));
|
||||||
@@ -249,7 +250,7 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onC
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="flex-1 truncate">{s.label || s.key}</span>
|
<span className="flex-1 truncate">{sessionDisplayName(s)}</span>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => togglePin(s.key, e)}
|
onClick={(e) => togglePin(s.key, e)}
|
||||||
className={`shrink-0 p-0.5 rounded-lg transition-all ${
|
className={`shrink-0 p-0.5 rounded-lg transition-all ${
|
||||||
|
|||||||
49
src/lib/sessionName.ts
Normal file
49
src/lib/sessionName.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import type { Session } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive a human-friendly display name for a session.
|
||||||
|
*
|
||||||
|
* Priority:
|
||||||
|
* 1. label (if set, e.g. sub-agent labels)
|
||||||
|
* 2. "Main" for kind=main
|
||||||
|
* 3. Kind + channel (e.g. "Cron · telegram")
|
||||||
|
* 4. Channel name capitalized
|
||||||
|
* 5. Cleaned session key (strip agent:xxx: prefix, truncate UUIDs)
|
||||||
|
*/
|
||||||
|
export function sessionDisplayName(session: Session): string {
|
||||||
|
if (session.label) return session.label;
|
||||||
|
|
||||||
|
const kind = session.kind;
|
||||||
|
const channel = session.channel;
|
||||||
|
|
||||||
|
if (kind === 'main') {
|
||||||
|
return channel ? `Main · ${capitalize(channel)}` : 'Main';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind === 'cron') {
|
||||||
|
return channel ? `Cron · ${capitalize(channel)}` : 'Cron';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind === 'isolated') {
|
||||||
|
return channel ? `Task · ${capitalize(channel)}` : 'Task';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel) return capitalize(channel);
|
||||||
|
|
||||||
|
// Fallback: clean the session key
|
||||||
|
return cleanSessionKey(session.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function capitalize(s: string): string {
|
||||||
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanSessionKey(key: string): string {
|
||||||
|
// Strip "agent:<id>:" prefix
|
||||||
|
const stripped = key.replace(/^agent:[^:]+:/, '');
|
||||||
|
// If it looks like a UUID, show first 8 chars
|
||||||
|
if (/^[0-9a-f]{8}-[0-9a-f]{4}-/.test(stripped)) {
|
||||||
|
return stripped.slice(0, 8) + '…';
|
||||||
|
}
|
||||||
|
return stripped;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user