From 08fb6d45f363ce5d2f2f3501b5910dfc83e766ae Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Thu, 18 Jun 2026 20:28:03 -0400 Subject: [PATCH] View model interactions (#9806) Signed-off-by: Douwe M Osinga Co-authored-by: Douwe M Osinga Co-authored-by: Michael Neale --- ui/desktop/src/__tests__/sessions.test.ts | 52 ++++- ui/desktop/src/components/BaseChat.tsx | 3 +- .../src/components/Layout/AppLayout.tsx | 42 ++-- .../src/components/Layout/NavigationPanel.tsx | 23 +- .../src/components/SessionActionsHeader.tsx | 214 ++++++++++++++++-- ui/desktop/src/hooks/useAcpChatSession.ts | 26 ++- ui/desktop/src/hooks/useChatSessionTypes.ts | 1 + ui/desktop/src/hooks/useChatStream.ts | 54 ++++- ui/desktop/src/i18n/messages/en.json | 42 +++- ui/desktop/src/i18n/messages/es.json | 42 +++- ui/desktop/src/i18n/messages/hi.json | 42 +++- ui/desktop/src/i18n/messages/ja.json | 62 +++-- ui/desktop/src/i18n/messages/ko.json | 42 +++- ui/desktop/src/i18n/messages/ru.json | 42 +++- ui/desktop/src/i18n/messages/tr.json | 42 +++- ui/desktop/src/i18n/messages/zh-CN.json | 42 +++- ui/desktop/src/sessions.ts | 15 +- ui/desktop/src/vite-env.d.ts | 6 +- 18 files changed, 676 insertions(+), 116 deletions(-) diff --git a/ui/desktop/src/__tests__/sessions.test.ts b/ui/desktop/src/__tests__/sessions.test.ts index e76650282b..e865e20190 100644 --- a/ui/desktop/src/__tests__/sessions.test.ts +++ b/ui/desktop/src/__tests__/sessions.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { getSessionDisplayName } from '../sessions'; +import { getSessionDisplayName, shouldShowNewChatTitle } from '../sessions'; import { prependUnique } from '../hooks/useNavigationSessions'; import type { Session } from '../api'; import type { SessionListItem } from '../acp/sessions'; @@ -30,19 +30,51 @@ function makeListItem(overrides: Partial = {}): SessionListItem }; } -describe('getSessionDisplayName', () => { - it('returns the session name', () => { - const session = makeSession({ - name: 'My Chat', - }); - expect(getSessionDisplayName(session)).toBe('My Chat'); +describe('shouldShowNewChatTitle', () => { + it('returns true for an empty session without a user-set name', () => { + const session = makeSession({ message_count: 0, user_set_name: false }); + expect(shouldShowNewChatTitle(session)).toBe(true); }); - it('falls back to the default title when the session name is empty', () => { + it('returns false when the session has messages', () => { + const session = makeSession({ message_count: 3, user_set_name: false }); + expect(shouldShowNewChatTitle(session)).toBe(false); + }); + + it('returns false when the user has set a custom name', () => { + const session = makeSession({ message_count: 0, user_set_name: true }); + expect(shouldShowNewChatTitle(session)).toBe(false); + }); + + it('returns false when the session has a recipe', () => { const session = makeSession({ - name: '', + message_count: 0, + user_set_name: false, + recipe: { title: 'Recipe', steps: [] } as unknown as Session['recipe'], }); - expect(getSessionDisplayName(session)).toBe('New Chat'); + expect(shouldShowNewChatTitle(session)).toBe(false); + }); +}); + +describe('getSessionDisplayName (fix for #8865)', () => { + it('returns the user-set name for a recipe session that has been renamed', () => { + const session = makeSession({ + name: 'My Renamed Chat', + user_set_name: true, + message_count: 2, + recipe: { title: 'Some Recipe' } as unknown as Session['recipe'], + }); + expect(getSessionDisplayName(session)).toBe('My Renamed Chat'); + }); + + it('falls back to the recipe title when the user has not renamed', () => { + const session = makeSession({ + name: 'auto-generated', + user_set_name: false, + message_count: 2, + recipe: { title: 'Some Recipe' } as unknown as Session['recipe'], + }); + expect(getSessionDisplayName(session)).toBe('Some Recipe'); }); }); diff --git a/ui/desktop/src/components/BaseChat.tsx b/ui/desktop/src/components/BaseChat.tsx index fc1cb8210b..33c6457497 100644 --- a/ui/desktop/src/components/BaseChat.tsx +++ b/ui/desktop/src/components/BaseChat.tsx @@ -106,6 +106,7 @@ export default function BaseChat({ messages, chatState, setChatState, + updateSession, handleSubmit, submitElicitationResponse, stopStreaming, @@ -434,7 +435,7 @@ export default function BaseChat({ - + = ({ activeSessions }) = const needsTrafficLightInset = safeIsMacOS && !isFullScreen; const headerPadding = needsTrafficLightInset ? 'pl-[96px]' : 'pl-4'; - const headerTop = needsTrafficLightInset ? 'top-[15px]' : 'top-[11px]'; + const headerTop = needsTrafficLightInset ? 'top-[14px]' : 'top-[11px]'; + const navToggleTitle = intl.formatMessage( + isNavExpanded ? i18n.collapseNavigation : i18n.openNavigation + ); return (
- {/* Floating menu toggle — only when sidebar is collapsed. When expanded, - the sidebar's own header has the collapse button. */} - {!isNavExpanded && ( -
+ -
- )} + {isNavExpanded ? : } + +
{/* Main content with navigation. Shared white canvas; the sidebar is a rounded outlined card floating on it with breathing room. */} diff --git a/ui/desktop/src/components/Layout/NavigationPanel.tsx b/ui/desktop/src/components/Layout/NavigationPanel.tsx index 8104e116b8..6c92868fe4 100644 --- a/ui/desktop/src/components/Layout/NavigationPanel.tsx +++ b/ui/desktop/src/components/Layout/NavigationPanel.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useLocation } from 'react-router-dom'; -import { ChevronDown, ChevronRight, PanelLeft } from 'lucide-react'; +import { ChevronDown, ChevronRight } from 'lucide-react'; import { motion } from 'framer-motion'; import { useNavigationContext } from './NavigationContext'; import { useConfig } from '../ConfigContext'; @@ -12,7 +12,6 @@ import { type NavItem, } from '../../hooks/useNavigationItems'; import { AppEvents } from '../../constants/events'; -import { Goose } from '../icons/Goose'; import { InlineEditText } from '../common/InlineEditText'; import { SessionIndicators } from '../SessionIndicators'; import { acpRenameSession, type SessionListItem } from '../../acp/sessions'; @@ -39,10 +38,6 @@ const i18n = defineMessages({ id: 'navigationPanel.untitledSession', defaultMessage: 'Untitled session', }, - collapseSidebar: { - id: 'navigationPanel.collapseSidebar', - defaultMessage: 'Collapse sidebar', - }, }); const navItemClass = (active: boolean) => @@ -124,7 +119,7 @@ const SessionRow: React.FC = ({ session, active, status, onClic export const Navigation: React.FC<{ className?: string }> = ({ className }) => { const intl = useIntl(); - const { isNavExpanded, setIsNavExpanded } = useNavigationContext(); + const { isNavExpanded } = useNavigationContext(); const location = useLocation(); const { extensionsList } = useConfig(); @@ -198,20 +193,10 @@ export const Navigation: React.FC<{ className?: string }> = ({ className }) => { transition={{ duration: 0.15 }} className={cn('bg-background-primary outline-none flex flex-col h-full', className)} > - {/* Header: logo + collapse button. Top padding clears the macOS traffic lights. */} -
- - -
+
{/* Nav items */} -
+
{visibleItems.map((item) => ( Session) => void; className?: string; } @@ -253,16 +309,82 @@ function JsonTree({ ); } -export default function SessionActionsHeader({ session, className }: SessionActionsHeaderProps) { +export default function SessionActionsHeader({ + session, + onSessionChange, + className, +}: SessionActionsHeaderProps) { const intl = useIntl(); + const [isRenameOpen, setIsRenameOpen] = useState(false); + const [renameValue, setRenameValue] = useState(''); + const [isRenaming, setIsRenaming] = useState(false); const [isJsonOpen, setIsJsonOpen] = useState(false); const [jsonValue, setJsonValue] = useState(null); const [jsonText, setJsonText] = useState(''); const [isJsonLoading, setIsJsonLoading] = useState(false); + const [isDuplicating, setIsDuplicating] = useState(false); const [fullTextSelection, setFullTextSelection] = useState(null); const title = useMemo(() => (session ? getSessionDisplayName(session) : ''), [session]); + useEffect(() => { + if (session && isRenameOpen) { + setRenameValue(getSessionDisplayName(session)); + } + }, [isRenameOpen, session]); + + const handleRename = useCallback(async () => { + if (!session || isRenaming) return; + + const trimmedName = renameValue.trim(); + if (!trimmedName) return; + + if (trimmedName === session.name) { + setIsRenameOpen(false); + return; + } + + setIsRenaming(true); + try { + await acpRenameSession(session.id, trimmedName); + onSessionChange((current) => ({ ...current, name: trimmedName, user_set_name: true })); + window.dispatchEvent( + new CustomEvent(AppEvents.SESSION_RENAMED, { + detail: { sessionId: session.id, newName: trimmedName, userInitiated: true }, + }) + ); + setIsRenameOpen(false); + toast.success(intl.formatMessage(i18n.renamed)); + } catch (error) { + toast.error( + intl.formatMessage(i18n.renameFailed, { + error: errorMessage(error, 'Unknown error'), + }) + ); + } finally { + setIsRenaming(false); + } + }, [intl, isRenaming, onSessionChange, renameValue, session]); + + const handleDuplicate = useCallback(async () => { + if (!session || isDuplicating) return; + + setIsDuplicating(true); + try { + await acpForkSession(session.id); + window.dispatchEvent(new CustomEvent(AppEvents.SESSION_CREATED)); + toast.success(intl.formatMessage(i18n.duplicated)); + } catch (error) { + toast.error( + intl.formatMessage(i18n.duplicateFailed, { + error: errorMessage(error, 'Unknown error'), + }) + ); + } finally { + setIsDuplicating(false); + } + }, [intl, isDuplicating, session]); + const handleViewJson = useCallback(async () => { if (!session) return; @@ -306,6 +428,15 @@ export default function SessionActionsHeader({ session, className }: SessionActi } }, []); + const handleRenameKeyDown = useCallback( + (event: KeyboardEvent) => { + if (event.key === 'Enter') { + void handleRename(); + } + }, + [handleRename] + ); + if (!session) { return null; } @@ -318,22 +449,69 @@ export default function SessionActionsHeader({ session, className }: SessionActi className )} > - + + + + + + setIsRenameOpen(true)}> + + {intl.formatMessage(i18n.renameSession)} + + void handleDuplicate()}> + {isDuplicating ? ( + + ) : ( + + )} + {intl.formatMessage(i18n.duplicateSession)} + + void handleViewJson()}> + {isJsonLoading ? ( + + ) : ( + + )} + {intl.formatMessage(i18n.viewJson)} + + +
+ + + + {intl.formatMessage(i18n.renameTitle)} + + setRenameValue(event.target.value)} + onKeyDown={handleRenameKeyDown} + placeholder={intl.formatMessage(i18n.renamePlaceholder)} + className="w-full rounded-lg border border-border-primary bg-background-primary p-3 text-text-primary outline-none focus:ring-2 focus:ring-border-active" + disabled={isRenaming} + maxLength={200} + autoFocus + /> + + + + + + + diff --git a/ui/desktop/src/hooks/useAcpChatSession.ts b/ui/desktop/src/hooks/useAcpChatSession.ts index 85805f2045..eef729bbad 100644 --- a/ui/desktop/src/hooks/useAcpChatSession.ts +++ b/ui/desktop/src/hooks/useAcpChatSession.ts @@ -192,8 +192,8 @@ export function useAcpChatSession({ useEffect(() => { const handleSessionRenamed = (event: Event) => { - const { sessionId: renamedSessionId, newName } = ( - event as CustomEvent<{ sessionId: string; newName: string }> + const { sessionId: renamedSessionId, newName, userInitiated } = ( + event as CustomEvent<{ sessionId: string; newName: string; userInitiated?: boolean }> ).detail; if (renamedSessionId !== sessionId) { @@ -201,11 +201,15 @@ export function useAcpChatSession({ } const currentSession = stateRef.current.session; - if (!currentSession || currentSession.name === newName) { + if (!currentSession || (currentSession.name === newName && !userInitiated)) { return; } - const updatedSession = { ...currentSession, name: newName }; + const updatedSession = { + ...currentSession, + name: newName, + ...(userInitiated && { user_set_name: true }), + }; acpChatSessionStore.setSessionMetadata(sessionId, updatedSession); dispatch({ type: 'SET_SESSION', payload: updatedSession }); }; @@ -567,6 +571,19 @@ export function useAcpChatSession({ [sessionId] ); + const updateSession = useCallback( + (updater: (session: Session) => Session) => { + const cached = acpChatSessionStore.getSnapshot(sessionId); + const currentSession = stateRef.current.session ?? cached?.session; + if (!currentSession) return; + + const nextSession = updater(currentSession); + acpChatSessionStore.setSessionMetadata(sessionId, nextSession); + dispatch({ type: 'SET_SESSION', payload: nextSession }); + }, + [sessionId] + ); + const cached = acpChatSessionStore.getSnapshot(sessionId); const maybe_cached_messages = state.session ? state.messages : cached?.messages || []; const maybe_cached_session = state.session ?? cached?.session; @@ -588,6 +605,7 @@ export function useAcpChatSession({ session: maybe_cached_session, chatState: state.chatState, setChatState, + updateSession, handleSubmit, submitElicitationResponse, stopStreaming, diff --git a/ui/desktop/src/hooks/useChatSessionTypes.ts b/ui/desktop/src/hooks/useChatSessionTypes.ts index 14f2e065f7..15822a2b3f 100644 --- a/ui/desktop/src/hooks/useChatSessionTypes.ts +++ b/ui/desktop/src/hooks/useChatSessionTypes.ts @@ -13,6 +13,7 @@ export interface UseChatSessionResult { messages: Message[]; chatState: ChatState; setChatState: (state: ChatState) => void; + updateSession: (updater: (session: Session) => Session) => void; handleSubmit: (input: UserInput) => Promise; submitElicitationResponse: ( elicitationId: string, diff --git a/ui/desktop/src/hooks/useChatStream.ts b/ui/desktop/src/hooks/useChatStream.ts index 339be423b9..e8002ef4ff 100644 --- a/ui/desktop/src/hooks/useChatStream.ts +++ b/ui/desktop/src/hooks/useChatStream.ts @@ -476,10 +476,16 @@ export function useChatStream({ throwOnError: true, }); if (response.data?.name) { + const nextMessageCount = + response.data.message_count ?? currentState.session?.message_count; dispatch({ type: 'SET_SESSION', payload: currentState.session - ? { ...currentState.session, name: response.data.name } + ? { + ...currentState.session, + name: response.data.name, + ...(nextMessageCount !== undefined && { message_count: nextMessageCount }), + } : undefined, }); window.dispatchEvent( @@ -873,21 +879,32 @@ export function useChatStream({ }); const currentState = stateRef.current; const currentName = currentState.session?.name; + const currentMessageCount = currentState.session?.message_count; const newName = response.data?.name; + const newMessageCount = response.data?.message_count; + const hasNameChange = Boolean(newName && newName !== currentName); + const hasMessageCountChange = + newMessageCount !== undefined && newMessageCount !== currentMessageCount; - if (newName && newName !== currentName) { + if (newName && (hasNameChange || hasMessageCountChange)) { dispatch({ type: 'SET_SESSION', payload: currentState.session - ? { ...currentState.session, name: newName } + ? { + ...currentState.session, + name: newName, + ...(newMessageCount !== undefined && { message_count: newMessageCount }), + } : undefined, }); - window.dispatchEvent( - new CustomEvent(AppEvents.SESSION_RENAMED, { - detail: { sessionId, newName }, - }) - ); - return; + if (hasNameChange) { + window.dispatchEvent( + new CustomEvent(AppEvents.SESSION_RENAMED, { + detail: { sessionId, newName }, + }) + ); + return; + } } } catch { // Silently continue polling @@ -1116,6 +1133,24 @@ export function useChatStream({ dispatch({ type: 'SET_CHAT_STATE', payload: newState }); }, []); + const updateSession = useCallback( + (updater: (session: Session) => Session) => { + const cached = resultsCache.get(sessionId); + const currentSession = stateRef.current.session ?? cached?.session; + if (!currentSession) return; + + const nextSession = updater(currentSession); + dispatch({ type: 'SET_SESSION', payload: nextSession }); + resultsCache.set(sessionId, { + session: nextSession, + messages: stateRef.current.messages.length + ? stateRef.current.messages + : cached?.messages || [], + }); + }, + [sessionId] + ); + const cached = resultsCache.get(sessionId); const maybe_cached_messages = state.session ? state.messages : cached?.messages || []; const maybe_cached_session = state.session ?? cached?.session; @@ -1137,6 +1172,7 @@ export function useChatStream({ session: maybe_cached_session, chatState: state.chatState, setChatState, + updateSession, handleSubmit, submitElicitationResponse, stopStreaming, diff --git a/ui/desktop/src/i18n/messages/en.json b/ui/desktop/src/i18n/messages/en.json index 42834b79b8..f5a8eaa1e8 100644 --- a/ui/desktop/src/i18n/messages/en.json +++ b/ui/desktop/src/i18n/messages/en.json @@ -11,6 +11,9 @@ "announcementModal.gotIt": { "defaultMessage": "Got it!" }, + "appLayout.collapseNavigation": { + "defaultMessage": "Collapse navigation" + }, "appLayout.openNavigation": { "defaultMessage": "Open navigation" }, @@ -2465,9 +2468,6 @@ "navigationPanel.chats": { "defaultMessage": "Chats" }, - "navigationPanel.collapseSidebar": { - "defaultMessage": "Collapse sidebar" - }, "navigationPanel.noChats": { "defaultMessage": "No recent chats" }, @@ -3758,6 +3758,12 @@ "securityToggle.warpNotice": { "defaultMessage": "Command injection detection works best when connected to WARP (required to reach the classification service)." }, + "sessionActionsHeader.actionsLabel": { + "defaultMessage": "Session actions" + }, + "sessionActionsHeader.cancel": { + "defaultMessage": "Cancel" + }, "sessionActionsHeader.close": { "defaultMessage": "Close" }, @@ -3773,6 +3779,15 @@ "sessionActionsHeader.copyText": { "defaultMessage": "Copy text" }, + "sessionActionsHeader.duplicateFailed": { + "defaultMessage": "Failed to duplicate session: {error}" + }, + "sessionActionsHeader.duplicateSession": { + "defaultMessage": "Duplicate session" + }, + "sessionActionsHeader.duplicated": { + "defaultMessage": "Session duplicated" + }, "sessionActionsHeader.fullTextTitle": { "defaultMessage": "Text value" }, @@ -3785,6 +3800,27 @@ "sessionActionsHeader.loadingJson": { "defaultMessage": "Loading JSON..." }, + "sessionActionsHeader.renameFailed": { + "defaultMessage": "Failed to rename session: {error}" + }, + "sessionActionsHeader.renamePlaceholder": { + "defaultMessage": "Enter session name" + }, + "sessionActionsHeader.renameSession": { + "defaultMessage": "Rename session" + }, + "sessionActionsHeader.renameTitle": { + "defaultMessage": "Rename Session" + }, + "sessionActionsHeader.renamed": { + "defaultMessage": "Session renamed" + }, + "sessionActionsHeader.save": { + "defaultMessage": "Save" + }, + "sessionActionsHeader.saving": { + "defaultMessage": "Saving..." + }, "sessionActionsHeader.viewJson": { "defaultMessage": "View session JSON" }, diff --git a/ui/desktop/src/i18n/messages/es.json b/ui/desktop/src/i18n/messages/es.json index 4959ba4bc8..14fd9d2b34 100644 --- a/ui/desktop/src/i18n/messages/es.json +++ b/ui/desktop/src/i18n/messages/es.json @@ -11,6 +11,9 @@ "announcementModal.gotIt": { "defaultMessage": "¡Entendido!" }, + "appLayout.collapseNavigation": { + "defaultMessage": "Contraer navegación" + }, "appLayout.openNavigation": { "defaultMessage": "Abrir navegación" }, @@ -2465,9 +2468,6 @@ "navigationPanel.chats": { "defaultMessage": "Chats" }, - "navigationPanel.collapseSidebar": { - "defaultMessage": "Contraer barra lateral" - }, "navigationPanel.noChats": { "defaultMessage": "No hay chats recientes" }, @@ -3758,6 +3758,12 @@ "securityToggle.warpNotice": { "defaultMessage": "La detección de inyección de comandos funciona mejor cuando estás conectado a WARP (necesario para llegar al servicio de clasificación)." }, + "sessionActionsHeader.actionsLabel": { + "defaultMessage": "Acciones de la sesión" + }, + "sessionActionsHeader.cancel": { + "defaultMessage": "Cancelar" + }, "sessionActionsHeader.close": { "defaultMessage": "Cerrar" }, @@ -3773,6 +3779,15 @@ "sessionActionsHeader.copyText": { "defaultMessage": "Copiar texto" }, + "sessionActionsHeader.duplicateFailed": { + "defaultMessage": "No se pudo duplicar la sesión: {error}" + }, + "sessionActionsHeader.duplicateSession": { + "defaultMessage": "Duplicar sesión" + }, + "sessionActionsHeader.duplicated": { + "defaultMessage": "Sesión duplicada" + }, "sessionActionsHeader.fullTextTitle": { "defaultMessage": "Valor del texto" }, @@ -3785,6 +3800,27 @@ "sessionActionsHeader.loadingJson": { "defaultMessage": "Cargando JSON..." }, + "sessionActionsHeader.renameFailed": { + "defaultMessage": "No se pudo renombrar la sesión: {error}" + }, + "sessionActionsHeader.renamePlaceholder": { + "defaultMessage": "Introduce el nombre de la sesión" + }, + "sessionActionsHeader.renameSession": { + "defaultMessage": "Renombrar sesión" + }, + "sessionActionsHeader.renameTitle": { + "defaultMessage": "Renombrar sesión" + }, + "sessionActionsHeader.renamed": { + "defaultMessage": "Sesión renombrada" + }, + "sessionActionsHeader.save": { + "defaultMessage": "Guardar" + }, + "sessionActionsHeader.saving": { + "defaultMessage": "Guardando..." + }, "sessionActionsHeader.viewJson": { "defaultMessage": "Ver el JSON de la sesión" }, diff --git a/ui/desktop/src/i18n/messages/hi.json b/ui/desktop/src/i18n/messages/hi.json index 8a15b3ce72..1d627c3bac 100644 --- a/ui/desktop/src/i18n/messages/hi.json +++ b/ui/desktop/src/i18n/messages/hi.json @@ -11,6 +11,9 @@ "announcementModal.gotIt": { "defaultMessage": "समझ गया!" }, + "appLayout.collapseNavigation": { + "defaultMessage": "Collapse navigation" + }, "appLayout.openNavigation": { "defaultMessage": "नेविगेशन खोलें" }, @@ -2465,9 +2468,6 @@ "navigationPanel.chats": { "defaultMessage": "चैट" }, - "navigationPanel.collapseSidebar": { - "defaultMessage": "साइडबार को संक्षिप्त करें" - }, "navigationPanel.noChats": { "defaultMessage": "कोई हालिया चैट नहीं" }, @@ -3758,6 +3758,12 @@ "securityToggle.warpNotice": { "defaultMessage": "WARP से कनेक्ट होने पर कमांड इंजेक्शन डिटेक्शन सबसे अच्छा काम करता है (वर्गीकरण सेवा तक पहुंचने के लिए आवश्यक)।" }, + "sessionActionsHeader.actionsLabel": { + "defaultMessage": "Session actions" + }, + "sessionActionsHeader.cancel": { + "defaultMessage": "Cancel" + }, "sessionActionsHeader.close": { "defaultMessage": "Close" }, @@ -3773,6 +3779,15 @@ "sessionActionsHeader.copyText": { "defaultMessage": "Copy text" }, + "sessionActionsHeader.duplicateFailed": { + "defaultMessage": "Failed to duplicate session: {error}" + }, + "sessionActionsHeader.duplicateSession": { + "defaultMessage": "Duplicate session" + }, + "sessionActionsHeader.duplicated": { + "defaultMessage": "Session duplicated" + }, "sessionActionsHeader.fullTextTitle": { "defaultMessage": "Text value" }, @@ -3785,6 +3800,27 @@ "sessionActionsHeader.loadingJson": { "defaultMessage": "Loading JSON..." }, + "sessionActionsHeader.renameFailed": { + "defaultMessage": "Failed to rename session: {error}" + }, + "sessionActionsHeader.renamePlaceholder": { + "defaultMessage": "Enter session name" + }, + "sessionActionsHeader.renameSession": { + "defaultMessage": "Rename session" + }, + "sessionActionsHeader.renameTitle": { + "defaultMessage": "Rename Session" + }, + "sessionActionsHeader.renamed": { + "defaultMessage": "Session renamed" + }, + "sessionActionsHeader.save": { + "defaultMessage": "Save" + }, + "sessionActionsHeader.saving": { + "defaultMessage": "Saving..." + }, "sessionActionsHeader.viewJson": { "defaultMessage": "View session JSON" }, diff --git a/ui/desktop/src/i18n/messages/ja.json b/ui/desktop/src/i18n/messages/ja.json index 0b64a68194..fcaf5ad406 100644 --- a/ui/desktop/src/i18n/messages/ja.json +++ b/ui/desktop/src/i18n/messages/ja.json @@ -11,6 +11,9 @@ "announcementModal.gotIt": { "defaultMessage": "了解しました" }, + "appLayout.collapseNavigation": { + "defaultMessage": "ナビを折りたたむ" + }, "appLayout.openNavigation": { "defaultMessage": "ナビゲーションを開く" }, @@ -2465,9 +2468,6 @@ "navigationPanel.chats": { "defaultMessage": "チャット" }, - "navigationPanel.collapseSidebar": { - "defaultMessage": "サイドバーを折りたたむ" - }, "navigationPanel.noChats": { "defaultMessage": "最近のチャットはありません" }, @@ -3758,35 +3758,71 @@ "securityToggle.warpNotice": { "defaultMessage": "コマンドインジェクション検出は、WARPに接続していると最も効果的です(分類サービスに到達するために必要です)。" }, + "sessionActionsHeader.actionsLabel": { + "defaultMessage": "セッションの操作" + }, + "sessionActionsHeader.cancel": { + "defaultMessage": "キャンセル" + }, "sessionActionsHeader.close": { - "defaultMessage": "Close" + "defaultMessage": "閉じる" }, "sessionActionsHeader.copiedJson": { - "defaultMessage": "Session JSON copied" + "defaultMessage": "セッションJSONをコピーしました" }, "sessionActionsHeader.copiedText": { - "defaultMessage": "Text copied" + "defaultMessage": "テキストをコピーしました" }, "sessionActionsHeader.copyJson": { - "defaultMessage": "Copy JSON" + "defaultMessage": "JSONをコピー" }, "sessionActionsHeader.copyText": { - "defaultMessage": "Copy text" + "defaultMessage": "テキストをコピー" + }, + "sessionActionsHeader.duplicateFailed": { + "defaultMessage": "セッションを複製できませんでした: {error}" + }, + "sessionActionsHeader.duplicateSession": { + "defaultMessage": "セッションを複製" + }, + "sessionActionsHeader.duplicated": { + "defaultMessage": "セッションを複製しました" }, "sessionActionsHeader.fullTextTitle": { - "defaultMessage": "Text value" + "defaultMessage": "テキスト値" }, "sessionActionsHeader.jsonFailed": { - "defaultMessage": "Failed to load session JSON: {error}" + "defaultMessage": "セッションJSONを読み込めませんでした: {error}" }, "sessionActionsHeader.jsonTitle": { - "defaultMessage": "Session JSON" + "defaultMessage": "セッションJSON" }, "sessionActionsHeader.loadingJson": { - "defaultMessage": "Loading JSON..." + "defaultMessage": "JSONを読み込み中..." + }, + "sessionActionsHeader.renameFailed": { + "defaultMessage": "セッションの名前を変更できませんでした: {error}" + }, + "sessionActionsHeader.renamePlaceholder": { + "defaultMessage": "セッション名を入力" + }, + "sessionActionsHeader.renameSession": { + "defaultMessage": "セッションの名前を変更" + }, + "sessionActionsHeader.renameTitle": { + "defaultMessage": "セッションの名前を変更" + }, + "sessionActionsHeader.renamed": { + "defaultMessage": "セッションの名前を変更しました" + }, + "sessionActionsHeader.save": { + "defaultMessage": "保存" + }, + "sessionActionsHeader.saving": { + "defaultMessage": "保存中..." }, "sessionActionsHeader.viewJson": { - "defaultMessage": "View session JSON" + "defaultMessage": "セッションJSONを表示" }, "sessionHistory.cancel": { "defaultMessage": "キャンセル" diff --git a/ui/desktop/src/i18n/messages/ko.json b/ui/desktop/src/i18n/messages/ko.json index cb0d37c633..c7bed63e39 100644 --- a/ui/desktop/src/i18n/messages/ko.json +++ b/ui/desktop/src/i18n/messages/ko.json @@ -11,6 +11,9 @@ "announcementModal.gotIt": { "defaultMessage": "알았어요!" }, + "appLayout.collapseNavigation": { + "defaultMessage": "탐색 접기" + }, "appLayout.openNavigation": { "defaultMessage": "탐색 열기" }, @@ -2465,9 +2468,6 @@ "navigationPanel.chats": { "defaultMessage": "채팅" }, - "navigationPanel.collapseSidebar": { - "defaultMessage": "사이드바 접기" - }, "navigationPanel.noChats": { "defaultMessage": "최근 채팅 없음" }, @@ -3758,6 +3758,12 @@ "securityToggle.warpNotice": { "defaultMessage": "명령 인젝션 감지는 WARP에 연결했을 때 가장 잘 작동합니다. 분류 서비스에 연결하려면 WARP가 필요합니다." }, + "sessionActionsHeader.actionsLabel": { + "defaultMessage": "세션 작업" + }, + "sessionActionsHeader.cancel": { + "defaultMessage": "취소" + }, "sessionActionsHeader.close": { "defaultMessage": "닫기" }, @@ -3773,6 +3779,15 @@ "sessionActionsHeader.copyText": { "defaultMessage": "텍스트 복사" }, + "sessionActionsHeader.duplicateFailed": { + "defaultMessage": "세션 복제 실패: {error}" + }, + "sessionActionsHeader.duplicateSession": { + "defaultMessage": "세션 복제" + }, + "sessionActionsHeader.duplicated": { + "defaultMessage": "세션이 복제되었습니다" + }, "sessionActionsHeader.fullTextTitle": { "defaultMessage": "텍스트 값" }, @@ -3785,6 +3800,27 @@ "sessionActionsHeader.loadingJson": { "defaultMessage": "JSON 불러오는 중..." }, + "sessionActionsHeader.renameFailed": { + "defaultMessage": "세션 이름 변경 실패: {error}" + }, + "sessionActionsHeader.renamePlaceholder": { + "defaultMessage": "세션 이름 입력" + }, + "sessionActionsHeader.renameSession": { + "defaultMessage": "세션 이름 변경" + }, + "sessionActionsHeader.renameTitle": { + "defaultMessage": "세션 이름 변경" + }, + "sessionActionsHeader.renamed": { + "defaultMessage": "세션 이름이 변경되었습니다" + }, + "sessionActionsHeader.save": { + "defaultMessage": "저장" + }, + "sessionActionsHeader.saving": { + "defaultMessage": "저장 중..." + }, "sessionActionsHeader.viewJson": { "defaultMessage": "세션 JSON 보기" }, diff --git a/ui/desktop/src/i18n/messages/ru.json b/ui/desktop/src/i18n/messages/ru.json index b3695ed865..0d5d538816 100644 --- a/ui/desktop/src/i18n/messages/ru.json +++ b/ui/desktop/src/i18n/messages/ru.json @@ -11,6 +11,9 @@ "announcementModal.gotIt": { "defaultMessage": "Понятно!" }, + "appLayout.collapseNavigation": { + "defaultMessage": "Collapse navigation" + }, "appLayout.openNavigation": { "defaultMessage": "Открыть навигацию" }, @@ -2465,9 +2468,6 @@ "navigationPanel.chats": { "defaultMessage": "Чаты" }, - "navigationPanel.collapseSidebar": { - "defaultMessage": "Свернуть боковую панель" - }, "navigationPanel.noChats": { "defaultMessage": "Нет недавних чатов" }, @@ -3758,6 +3758,12 @@ "securityToggle.warpNotice": { "defaultMessage": "Обнаружение внедрения команд работает лучше всего при подключении к WARP (требуется для доступа к сервису классификации)." }, + "sessionActionsHeader.actionsLabel": { + "defaultMessage": "Session actions" + }, + "sessionActionsHeader.cancel": { + "defaultMessage": "Cancel" + }, "sessionActionsHeader.close": { "defaultMessage": "Close" }, @@ -3773,6 +3779,15 @@ "sessionActionsHeader.copyText": { "defaultMessage": "Copy text" }, + "sessionActionsHeader.duplicateFailed": { + "defaultMessage": "Failed to duplicate session: {error}" + }, + "sessionActionsHeader.duplicateSession": { + "defaultMessage": "Duplicate session" + }, + "sessionActionsHeader.duplicated": { + "defaultMessage": "Session duplicated" + }, "sessionActionsHeader.fullTextTitle": { "defaultMessage": "Text value" }, @@ -3785,6 +3800,27 @@ "sessionActionsHeader.loadingJson": { "defaultMessage": "Loading JSON..." }, + "sessionActionsHeader.renameFailed": { + "defaultMessage": "Failed to rename session: {error}" + }, + "sessionActionsHeader.renamePlaceholder": { + "defaultMessage": "Enter session name" + }, + "sessionActionsHeader.renameSession": { + "defaultMessage": "Rename session" + }, + "sessionActionsHeader.renameTitle": { + "defaultMessage": "Rename Session" + }, + "sessionActionsHeader.renamed": { + "defaultMessage": "Session renamed" + }, + "sessionActionsHeader.save": { + "defaultMessage": "Save" + }, + "sessionActionsHeader.saving": { + "defaultMessage": "Saving..." + }, "sessionActionsHeader.viewJson": { "defaultMessage": "View session JSON" }, diff --git a/ui/desktop/src/i18n/messages/tr.json b/ui/desktop/src/i18n/messages/tr.json index d8c5f68618..9ccf3684a5 100644 --- a/ui/desktop/src/i18n/messages/tr.json +++ b/ui/desktop/src/i18n/messages/tr.json @@ -11,6 +11,9 @@ "announcementModal.gotIt": { "defaultMessage": "Anladım!" }, + "appLayout.collapseNavigation": { + "defaultMessage": "Collapse navigation" + }, "appLayout.openNavigation": { "defaultMessage": "Gezinmeyi aç" }, @@ -2465,9 +2468,6 @@ "navigationPanel.chats": { "defaultMessage": "Sohbetler" }, - "navigationPanel.collapseSidebar": { - "defaultMessage": "Kenar çubuğunu daralt" - }, "navigationPanel.noChats": { "defaultMessage": "Son sohbet yok" }, @@ -3758,6 +3758,12 @@ "securityToggle.warpNotice": { "defaultMessage": "Komut enjeksiyonu algılama, WARP bağlantısı olduğunda en iyi şekilde çalışır (sınıflandırma hizmetine erişmek için gereklidir)." }, + "sessionActionsHeader.actionsLabel": { + "defaultMessage": "Session actions" + }, + "sessionActionsHeader.cancel": { + "defaultMessage": "Cancel" + }, "sessionActionsHeader.close": { "defaultMessage": "Close" }, @@ -3773,6 +3779,15 @@ "sessionActionsHeader.copyText": { "defaultMessage": "Copy text" }, + "sessionActionsHeader.duplicateFailed": { + "defaultMessage": "Failed to duplicate session: {error}" + }, + "sessionActionsHeader.duplicateSession": { + "defaultMessage": "Duplicate session" + }, + "sessionActionsHeader.duplicated": { + "defaultMessage": "Session duplicated" + }, "sessionActionsHeader.fullTextTitle": { "defaultMessage": "Text value" }, @@ -3785,6 +3800,27 @@ "sessionActionsHeader.loadingJson": { "defaultMessage": "Loading JSON..." }, + "sessionActionsHeader.renameFailed": { + "defaultMessage": "Failed to rename session: {error}" + }, + "sessionActionsHeader.renamePlaceholder": { + "defaultMessage": "Enter session name" + }, + "sessionActionsHeader.renameSession": { + "defaultMessage": "Rename session" + }, + "sessionActionsHeader.renameTitle": { + "defaultMessage": "Rename Session" + }, + "sessionActionsHeader.renamed": { + "defaultMessage": "Session renamed" + }, + "sessionActionsHeader.save": { + "defaultMessage": "Save" + }, + "sessionActionsHeader.saving": { + "defaultMessage": "Saving..." + }, "sessionActionsHeader.viewJson": { "defaultMessage": "View session JSON" }, diff --git a/ui/desktop/src/i18n/messages/zh-CN.json b/ui/desktop/src/i18n/messages/zh-CN.json index 2fc08817f1..48ff07246b 100644 --- a/ui/desktop/src/i18n/messages/zh-CN.json +++ b/ui/desktop/src/i18n/messages/zh-CN.json @@ -11,6 +11,9 @@ "announcementModal.gotIt": { "defaultMessage": "知道了!" }, + "appLayout.collapseNavigation": { + "defaultMessage": "Collapse navigation" + }, "appLayout.openNavigation": { "defaultMessage": "展开导航" }, @@ -2465,9 +2468,6 @@ "navigationPanel.chats": { "defaultMessage": "聊天" }, - "navigationPanel.collapseSidebar": { - "defaultMessage": "折叠侧边栏" - }, "navigationPanel.noChats": { "defaultMessage": "没有最近的聊天" }, @@ -3758,6 +3758,12 @@ "securityToggle.warpNotice": { "defaultMessage": "连接到 WARP 时,命令注入检测效果最佳(访问分类服务需要 WARP)。" }, + "sessionActionsHeader.actionsLabel": { + "defaultMessage": "Session actions" + }, + "sessionActionsHeader.cancel": { + "defaultMessage": "Cancel" + }, "sessionActionsHeader.close": { "defaultMessage": "Close" }, @@ -3773,6 +3779,15 @@ "sessionActionsHeader.copyText": { "defaultMessage": "Copy text" }, + "sessionActionsHeader.duplicateFailed": { + "defaultMessage": "Failed to duplicate session: {error}" + }, + "sessionActionsHeader.duplicateSession": { + "defaultMessage": "Duplicate session" + }, + "sessionActionsHeader.duplicated": { + "defaultMessage": "Session duplicated" + }, "sessionActionsHeader.fullTextTitle": { "defaultMessage": "Text value" }, @@ -3785,6 +3800,27 @@ "sessionActionsHeader.loadingJson": { "defaultMessage": "Loading JSON..." }, + "sessionActionsHeader.renameFailed": { + "defaultMessage": "Failed to rename session: {error}" + }, + "sessionActionsHeader.renamePlaceholder": { + "defaultMessage": "Enter session name" + }, + "sessionActionsHeader.renameSession": { + "defaultMessage": "Rename session" + }, + "sessionActionsHeader.renameTitle": { + "defaultMessage": "Rename Session" + }, + "sessionActionsHeader.renamed": { + "defaultMessage": "Session renamed" + }, + "sessionActionsHeader.save": { + "defaultMessage": "Save" + }, + "sessionActionsHeader.saving": { + "defaultMessage": "Saving..." + }, "sessionActionsHeader.viewJson": { "defaultMessage": "View session JSON" }, diff --git a/ui/desktop/src/sessions.ts b/ui/desktop/src/sessions.ts index 06d10217a2..22269e964d 100644 --- a/ui/desktop/src/sessions.ts +++ b/ui/desktop/src/sessions.ts @@ -11,7 +11,20 @@ import { AppEvents } from './constants/events'; import { decodeRecipe, Recipe } from './recipe'; export function getSessionDisplayName(session: Session): string { - return session.name || DEFAULT_CHAT_TITLE; + if (session.user_set_name) { + return session.name; + } + if (session.recipe?.title) { + return session.recipe.title; + } + if (shouldShowNewChatTitle(session)) { + return DEFAULT_CHAT_TITLE; + } + return session.name; +} + +export function shouldShowNewChatTitle(session: Session): boolean { + return !session.user_set_name && session.message_count === 0 && !session.recipe?.title; } export function resumeSession(session: Session, setView: setViewType) { diff --git a/ui/desktop/src/vite-env.d.ts b/ui/desktop/src/vite-env.d.ts index 168573f676..d7c35b6a52 100644 --- a/ui/desktop/src/vite-env.d.ts +++ b/ui/desktop/src/vite-env.d.ts @@ -61,7 +61,11 @@ declare global { responseStyleChanged: CustomEvent; 'session-created': CustomEvent<{ session?: import('./api').Session }>; 'session-deleted': CustomEvent<{ sessionId: string }>; - 'session-renamed': CustomEvent<{ sessionId: string; newName: string }>; + 'session-renamed': CustomEvent<{ + sessionId: string; + newName: string; + userInitiated?: boolean; + }>; } }