feat: add /new slash command to create chat sessions

Introduces a `/new` slash command that lets users create a fresh chat
session for the current agent without leaving the chat input.

- Add `createNewSession` to `useGateway` that calls `sessions.create`
  on the gateway (with a client-side fallback key when the RPC is
  unavailable).
- Register `/new` in the slash-command menu with i18n descriptions
  across all 8 supported languages.
- Wire `onNewSession` through `Chat` → `ChatInput` so typing `/new`
  triggers session creation.
- Add `extractAgentIdFromKey` and `formatAgentId` helpers to
  `sessionName.ts` to derive a human-readable agent name from the
  session key (e.g. `agent:my-cool-bot:…` → "My Cool Bot").
- Use the new helpers in `Header` and `App` to show per-session agent
  names, especially for sub-agent sessions where the gateway-level
  identity differs from the session agent.

Made-with: Cursor
This commit is contained in:
Yaro
2026-02-28 22:43:20 +02:00
parent a11cebdc55
commit 4e66d7c4bd
9 changed files with 119 additions and 77 deletions

View File

@@ -18,8 +18,10 @@ interface Props {
status: ConnectionStatus;
sessionKey?: string;
onSend: (text: string, attachments?: Array<{ mimeType: string; fileName: string; content: string }>) => void;
onNewSession?: () => Promise<void>;
onAbort: () => void;
agentAvatarUrl?: string;
agentName?: string;
}
function isNoReply(msg: ChatMessage): boolean {
@@ -70,7 +72,7 @@ function getDateKey(ts: number): string {
/** Threshold in pixels — if the user is within this distance of the bottom, auto-scroll */
const SCROLL_THRESHOLD = 150;
export function Chat({ messages, isGenerating, isLoadingHistory, status, sessionKey, onSend, onAbort, agentAvatarUrl }: Props) {
export function Chat({ messages, isGenerating, isLoadingHistory, status, sessionKey, onSend, onNewSession, onAbort, agentAvatarUrl, agentName }: Props) {
const t = useT();
const bottomRef = useRef<HTMLDivElement>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null);
@@ -206,6 +208,8 @@ export function Chat({ messages, isGenerating, isLoadingHistory, status, session
}, [messages]);
const showTyping = isGenerating && !hasStreamedText(messages);
const sessionAgentId = sessionKey?.match(/^agent:([^:]+):/)?.[1];
const welcomeTitle = agentName || sessionAgentId || t('chat.welcome');
const { globalState, collapseAll, expandAll } = useToolCollapse();
const { toggle: toggleBookmark, isBookmarked, getForSession: getBookmarks } = useBookmarks();
@@ -291,7 +295,7 @@ export function Chat({ messages, isGenerating, isLoadingHistory, status, session
<Bot className="h-8 w-8 text-pc-accent-light" />
</div>
</div>
<div className="text-lg text-pc-text font-semibold">{t('chat.welcome')}</div>
<div className="text-lg text-pc-text font-semibold">{welcomeTitle}</div>
<div className="text-sm mt-1 text-pc-text-muted">{t('chat.welcomeSub')}</div>
<div className="mt-8 flex flex-col items-center gap-3 max-w-md w-full">
<div className="flex items-center gap-1.5 text-xs text-pc-text-faint">
@@ -422,7 +426,7 @@ export function Chat({ messages, isGenerating, isLoadingHistory, status, session
</div>
)}
</div>
<ChatInput onSend={handleSend} onAbort={onAbort} isGenerating={isGenerating} disabled={status !== 'connected'} sessionKey={sessionKey} replyTo={replyTo} onCancelReply={() => setReplyTo(null)} />
<ChatInput onSend={handleSend} onNewSession={onNewSession} onAbort={onAbort} isGenerating={isGenerating} disabled={status !== 'connected'} sessionKey={sessionKey} replyTo={replyTo} onCancelReply={() => setReplyTo(null)} />
</div>
);
}

View File

@@ -24,6 +24,7 @@ export interface ReplyContext {
interface Props {
onSend: (text: string, attachments?: Array<{ mimeType: string; fileName: string; content: string }>) => void;
onNewSession?: () => Promise<void>;
onAbort: () => void;
isGenerating: boolean;
disabled: boolean;
@@ -94,7 +95,7 @@ function formatSize(bytes: number): string {
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
}
export function ChatInput({ onSend, onAbort, isGenerating, disabled, sessionKey, replyTo, onCancelReply }: Props) {
export function ChatInput({ onSend, onNewSession, onAbort, isGenerating, disabled, sessionKey, replyTo, onCancelReply }: Props) {
const t = useT();
const { sendOnEnter, toggle: toggleSendShortcut } = useSendShortcut();
const [text, setText] = useState('');
@@ -177,6 +178,17 @@ export function ChatInput({ onSend, onAbort, isGenerating, disabled, sessionKey,
const handleSubmit = () => {
const trimmed = text.trim();
if ((!trimmed && files.length === 0) || disabled) return;
if ((trimmed === '/new' || trimmed.startsWith('/new ')) && onNewSession) {
void onNewSession();
setText('');
setFiles([]);
setShowSlash(false);
onCancelReply?.();
if (sessionKey) draftsRef.current.delete(sessionKey);
return;
}
const attachments = files.length > 0 ? files.map(f => ({
mimeType: f.mimeType,
fileName: f.file.name,

View File

@@ -4,7 +4,7 @@ import type { ConnectionStatus, Session, ChatMessage } from '../types';
import { useT } from '../hooks/useLocale';
import { SettingsModal } from './SettingsModal';
import { copyToClipboard } from '../lib/clipboard';
import { sessionDisplayName } from '../lib/sessionName';
import { sessionDisplayName, extractAgentIdFromKey, formatAgentId } from '../lib/sessionName';
import { messagesToMarkdown, downloadFile } from '../lib/exportChat';
interface Props {
@@ -24,6 +24,8 @@ interface Props {
export function Header({ status, sessionKey, onToggleSidebar, activeSessionData, onLogout, soundEnabled, onToggleSound, messages, agentAvatarUrl, agentName, onCompact }: Props) {
const t = useT();
const sessionLabel = activeSessionData ? sessionDisplayName(activeSessionData) : (sessionKey.split(':').pop() || sessionKey);
const sessionAgentId = activeSessionData?.agentId || extractAgentIdFromKey(sessionKey);
const headerAgentName = agentName || (sessionAgentId && formatAgentId(sessionAgentId)) || t('header.title');
const [showSessionInfo, setShowSessionInfo] = useState(false);
const [settingsOpen, setSettingsOpen] = useState(false);
const sessionInfoRef = useRef<HTMLDivElement>(null);
@@ -64,7 +66,7 @@ export function Header({ status, sessionKey, onToggleSidebar, activeSessionData,
<img src={agentAvatarUrl || '/logo.png'} alt="PinchChat" className="h-9 w-9 rounded-2xl object-cover" onError={(e) => { const img = e.target as HTMLImageElement; if (img.src !== window.location.origin + '/logo.png') { img.src = '/logo.png'; } else { img.style.display = 'none'; } }} />
<button className="min-w-0 text-left group" onClick={() => setShowSessionInfo(v => !v)} title={t('header.sessionInfo')} aria-label={t('header.sessionInfo')}>
<div className="flex items-center gap-2">
<span className="font-semibold text-pc-text text-sm tracking-wide">{agentName || t('header.title')}</span>
<span className="font-semibold text-pc-text text-sm tracking-wide">{headerAgentName}</span>
<Sparkles className="h-3.5 w-3.5 text-pc-accent-light/60" />
</div>
<span className="text-xs text-pc-text-muted truncate flex items-center gap-1.5">

View File

@@ -9,6 +9,7 @@ export interface SlashCommand {
}
const COMMANDS: SlashCommand[] = [
{ command: '/new', descKey: 'slash.new' },
{ command: '/status', descKey: 'slash.status' },
{ command: '/reasoning', args: 'on|off|stream', descKey: 'slash.reasoning' },
{ command: '/verbose', args: 'on|off', descKey: 'slash.verbose' },