refactor: remove dead component and useNavigationSessions cleanup (#9603)

This commit is contained in:
Lifei Zhou
2026-06-05 16:55:19 +10:00
committed by GitHub
parent ec519eeaaf
commit 65b72b2988
9 changed files with 81 additions and 479 deletions
+61 -65
View File
@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { shouldShowNewChatTitle } from '../sessions';
import { getSessionDisplayName } from '../hooks/useNavigationSessions';
import { getSessionDisplayName, sortAndTrim, prependUnique } from '../hooks/useNavigationSessions';
import type { Session } from '../api';
// Helper to build a minimal Session object for testing.
@@ -43,70 +43,6 @@ describe('shouldShowNewChatTitle', () => {
});
});
describe('session reuse scoping (fix for #7601)', () => {
// Simulates the core logic extracted from handleNewChat in useNavigationSessions.ts.
// Before the fix: `sessions.find(s => shouldShowNewChatTitle(s))` picked the
// first global empty session regardless of which window called it.
// After the fix: only the current window's activeSessionId is considered.
function findReusableSession(
sessions: Session[],
activeSessionId: string | undefined
): Session | undefined {
const currentActive = activeSessionId
? sessions.find((s) => s.id === activeSessionId)
: undefined;
if (currentActive && shouldShowNewChatTitle(currentActive)) {
return currentActive;
}
return undefined;
}
const emptySessionA = makeSession({ id: 'empty-a', message_count: 0, user_set_name: false });
const emptySessionB = makeSession({ id: 'empty-b', message_count: 0, user_set_name: false });
const usedSession = makeSession({ id: 'used-c', message_count: 5, user_set_name: true });
const allSessions = [emptySessionA, emptySessionB, usedSession];
it("window A only reuses its own active empty session, not window B's", () => {
// Window A has emptySessionA active, Window B has emptySessionB active.
// Under the old logic, both would grab emptySessionA (the first in the list).
const windowAResult = findReusableSession(allSessions, 'empty-a');
const windowBResult = findReusableSession(allSessions, 'empty-b');
expect(windowAResult?.id).toBe('empty-a');
expect(windowBResult?.id).toBe('empty-b');
// They never collide on the same session.
expect(windowAResult?.id).not.toBe(windowBResult?.id);
});
it('does not reuse a session that has messages even if it is active', () => {
const result = findReusableSession(allSessions, 'used-c');
expect(result).toBeUndefined();
});
it('returns undefined when there is no active session id', () => {
const result = findReusableSession(allSessions, undefined);
expect(result).toBeUndefined();
});
it('returns undefined when the active session id is not in the list', () => {
const result = findReusableSession(allSessions, 'nonexistent');
expect(result).toBeUndefined();
});
it('demonstrates the old bug: global find would give same session to both windows', () => {
// Old logic (before fix) - both windows get the same session.
const oldLogicFind = (sessions: Session[]) => sessions.find((s) => shouldShowNewChatTitle(s));
const windowAOld = oldLogicFind(allSessions);
const windowBOld = oldLogicFind(allSessions);
// Both windows would grab the exact same session - the bug.
expect(windowAOld?.id).toBe(windowBOld?.id);
expect(windowAOld?.id).toBe('empty-a');
});
});
describe('getSessionDisplayName (fix for #8865)', () => {
it('returns the user-set name for a recipe session that has been renamed', () => {
const session = makeSession({
@@ -128,3 +64,63 @@ describe('getSessionDisplayName (fix for #8865)', () => {
expect(getSessionDisplayName(session)).toBe('Some Recipe');
});
});
describe('sortAndTrim', () => {
it('sorts by updated_at descending', () => {
const result = sortAndTrim([
makeSession({
id: 'old-but-active',
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-03-01T00:00:00Z',
}),
makeSession({
id: 'newer-but-idle',
created_at: '2024-03-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
}),
makeSession({
id: 'mid',
created_at: '2024-02-01T00:00:00Z',
updated_at: '2024-02-01T00:00:00Z',
}),
]);
expect(result.map((s) => s.id)).toEqual(['old-but-active', 'mid', 'newer-but-idle']);
});
it('caps the list at 25 sessions', () => {
const sessions = Array.from({ length: 40 }, (_, i) =>
makeSession({ id: `s-${i}`, created_at: new Date(2024, 0, i + 1).toISOString() })
);
expect(sortAndTrim(sessions)).toHaveLength(25);
});
it('does not mutate the input array', () => {
const input = [
makeSession({ id: 'a', updated_at: '2024-01-01T00:00:00Z' }),
makeSession({ id: 'b', updated_at: '2024-02-01T00:00:00Z' }),
];
sortAndTrim(input);
expect(input.map((s) => s.id)).toEqual(['a', 'b']);
});
});
describe('prependUnique', () => {
it('prepends a new session to the front', () => {
const prev = [makeSession({ id: 'a' })];
const result = prependUnique(prev, makeSession({ id: 'b' }));
expect(result.map((s) => s.id)).toEqual(['b', 'a']);
});
it('returns the same reference when the session is already present', () => {
const prev = [makeSession({ id: 'a' }), makeSession({ id: 'b' })];
const result = prependUnique(prev, makeSession({ id: 'a' }));
expect(result).toBe(prev);
});
it('caps the list at 25 sessions', () => {
const prev = Array.from({ length: 25 }, (_, i) => makeSession({ id: `s-${i}` }));
const result = prependUnique(prev, makeSession({ id: 'new' }));
expect(result).toHaveLength(25);
expect(result[0].id).toBe('new');
});
});
@@ -1,119 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, type RenderOptions, screen, waitFor, fireEvent } from '@testing-library/react';
import { BottomMenuModeSelection } from './BottomMenuModeSelection';
import { IntlTestWrapper } from '../../i18n/test-utils';
const renderWithIntl = (ui: React.ReactElement, options?: RenderOptions) =>
render(ui, { wrapper: IntlTestWrapper, ...options });
let mockConfig: Record<string, unknown> = {};
const mockUpdateSession = vi.fn().mockResolvedValue({});
const mockGetSession = vi.fn().mockResolvedValue({ data: null });
vi.mock('../ConfigContext', () => ({
useConfig: () => ({
config: mockConfig,
}),
}));
vi.mock('../../utils/analytics', () => ({
trackModeChanged: vi.fn(),
}));
vi.mock('../../api', () => ({
updateSession: (...args: unknown[]) => mockUpdateSession(...args),
getSession: (...args: unknown[]) => mockGetSession(...args),
}));
// Radix dropdown doesn't open in jsdom — render children directly
vi.mock('../ui/dropdown-menu', () => ({
DropdownMenu: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DropdownMenuTrigger: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DropdownMenuContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DropdownMenuItem: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
describe('BottomMenuModeSelection', () => {
beforeEach(() => {
vi.clearAllMocks();
mockConfig = {};
});
it('displays mode from config when no session', async () => {
mockConfig.GOOSE_MODE = 'approve';
renderWithIntl(<BottomMenuModeSelection sessionId={null} />);
await waitFor(() => {
expect(screen.getByText('manual')).toBeInTheDocument();
});
});
it('defaults to auto when config has no mode', async () => {
mockConfig.GOOSE_MODE = undefined;
renderWithIntl(<BottomMenuModeSelection sessionId={null} />);
await waitFor(() => {
expect(screen.getByText('autonomous')).toBeInTheDocument();
});
});
it('fetches mode from session when sessionId is present', async () => {
mockConfig.GOOSE_MODE = 'auto';
mockGetSession.mockResolvedValue({ data: { goose_mode: 'approve' } });
renderWithIntl(<BottomMenuModeSelection sessionId="test-session-123" />);
await waitFor(() => {
expect(screen.getByText('manual')).toBeInTheDocument();
});
expect(mockGetSession).toHaveBeenCalledWith({
path: { session_id: 'test-session-123' },
});
});
it('calls updateSession and does not write global config', async () => {
mockConfig.GOOSE_MODE = 'auto';
renderWithIntl(<BottomMenuModeSelection sessionId="test-session-123" />);
fireEvent.click(screen.getByText('Manual'));
await waitFor(() => {
expect(mockUpdateSession).toHaveBeenCalledWith({
body: { session_id: 'test-session-123', goose_mode: 'approve' },
});
});
});
it('does not call updateSession when sessionId is null', async () => {
mockConfig.GOOSE_MODE = 'auto';
renderWithIntl(<BottomMenuModeSelection sessionId={null} />);
fireEvent.click(screen.getByText('Manual'));
await waitFor(() => {
expect(screen.getByText('manual')).toBeInTheDocument();
});
expect(mockUpdateSession).not.toHaveBeenCalled();
});
it('ignores stale session fetch after sessionId changes', async () => {
let resolveA: (value: unknown) => void;
const promiseA = new Promise((resolve) => {
resolveA = resolve;
});
mockGetSession
.mockImplementationOnce(() => promiseA)
.mockResolvedValueOnce({ data: { goose_mode: 'auto' } });
const { rerender } = renderWithIntl(<BottomMenuModeSelection sessionId="session-A" />);
rerender(<BottomMenuModeSelection sessionId="session-B" />);
await waitFor(() => {
expect(screen.getByText('autonomous')).toBeInTheDocument();
});
resolveA!({ data: { goose_mode: 'approve' } });
await waitFor(() => {
expect(screen.getByText('autonomous')).toBeInTheDocument();
});
expect(screen.queryByText('manual')).not.toBeInTheDocument();
});
});
@@ -1,108 +0,0 @@
import { useEffect, useState } from 'react';
import { Tornado } from 'lucide-react';
import { all_goose_modes, ModeSelectionItem } from '../settings/mode/ModeSelectionItem';
import { useConfig } from '../ConfigContext';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '../ui/dropdown-menu';
import { trackModeChanged } from '../../utils/analytics';
import { getSession, updateSession } from '../../api';
import { defineMessages, useIntl } from '../../i18n';
const i18n = defineMessages({
autoFallback: {
id: 'bottomMenuModeSelection.autoFallback',
defaultMessage: 'auto',
},
automaticModeDescription: {
id: 'bottomMenuModeSelection.automaticModeDescription',
defaultMessage: 'Automatic mode selection',
},
currentModeTitle: {
id: 'bottomMenuModeSelection.currentModeTitle',
defaultMessage: 'Current mode: {label} - {description}',
},
});
export const BottomMenuModeSelection = ({ sessionId }: { sessionId: string | null }) => {
const intl = useIntl();
const [gooseMode, setGooseMode] = useState('auto');
const { config } = useConfig();
useEffect(() => {
let cancelled = false;
if (sessionId) {
getSession({ path: { session_id: sessionId } }).then((res) => {
if (!cancelled && res.data?.goose_mode) {
setGooseMode(res.data.goose_mode);
}
});
} else {
const mode = config.GOOSE_MODE as string | undefined;
if (mode) {
setGooseMode(mode);
}
}
return () => {
cancelled = true;
};
}, [sessionId, config.GOOSE_MODE]);
const handleModeChange = async (newMode: string) => {
if (gooseMode === newMode) {
return;
}
try {
if (sessionId) {
await updateSession({ body: { session_id: sessionId, goose_mode: newMode } });
}
setGooseMode(newMode);
trackModeChanged(gooseMode, newMode);
} catch (error) {
console.error('Error updating goose mode:', error);
throw new Error(`Failed to store new goose mode: ${newMode}`);
}
};
function getValueByKey(key: string): string {
const mode = all_goose_modes.find((mode) => mode.key === key);
if (!mode) return intl.formatMessage(i18n.autoFallback);
return intl.formatMessage(mode.labelDescriptor);
}
function getModeDescription(key: string): string {
const mode = all_goose_modes.find((mode) => mode.key === key);
if (!mode) return intl.formatMessage(i18n.automaticModeDescription);
return intl.formatMessage(mode.descriptionDescriptor);
}
return (
<div title={intl.formatMessage(i18n.currentModeTitle, { label: getValueByKey(gooseMode), description: getModeDescription(gooseMode) })}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<span className="flex items-center cursor-pointer [&_svg]:size-4 text-text-primary/70 hover:text-text-primary hover:scale-100 hover:bg-transparent text-xs">
<Tornado className="mr-1 h-4 w-4" />
{getValueByKey(gooseMode).toLowerCase()}
</span>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-64" side="top" align="center">
{all_goose_modes.map((mode) => (
<DropdownMenuItem key={mode.key} asChild>
<ModeSelectionItem
mode={mode}
currentMode={gooseMode}
showDescription={false}
isApproveModeConfigure={false}
handleModeChange={handleModeChange}
/>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};
@@ -1,39 +0,0 @@
import React from 'react';
import { defineMessages, useIntl } from '../../i18n';
import { Card } from '../ui/card';
import { formatDate } from '../../utils/date';
import { Session } from '../../api';
import { shouldShowNewChatTitle } from '../../sessions';
import { DEFAULT_CHAT_TITLE } from '../../contexts/ChatContext';
const i18n = defineMessages({
messageCount: {
id: 'sessionItem.messageCount',
defaultMessage: '{count} messages',
},
});
interface SessionItemProps {
session: Session;
extraActions?: React.ReactNode;
}
const SessionItem: React.FC<SessionItemProps> = ({ session, extraActions }) => {
const intl = useIntl();
const displayName = shouldShowNewChatTitle(session) ? DEFAULT_CHAT_TITLE : session.name;
return (
<Card className="p-4 mb-2 hover:bg-background-inverse/50 cursor-pointer flex justify-between items-center">
<div>
<div className="font-medium">{displayName}</div>
<div className="text-sm text-muted-foreground">
{formatDate(session.updated_at)} {intl.formatMessage(i18n.messageCount, { count: session.message_count })}
</div>
<div className="text-sm text-muted-foreground">{session.working_dir}</div>
</div>
{extraActions && <div>{extraActions}</div>}
</Card>
);
};
export default SessionItem;
+20 -100
View File
@@ -2,43 +2,36 @@ import { useState, useEffect, useRef, useCallback } from 'react';
import { useNavigate, useLocation, useSearchParams } from 'react-router-dom';
import { getSession, listSessions } from '../api';
import { useChatContext } from '../contexts/ChatContext';
import { useConfig } from '../components/ConfigContext';
import { useNavigation } from './useNavigation';
import { startNewSession, resumeSession, shouldShowNewChatTitle } from '../sessions';
import { getInitialWorkingDir } from '../utils/workingDir';
import { shouldShowNewChatTitle } from '../sessions';
import { AppEvents } from '../constants/events';
import type { Session } from '../api';
const MAX_RECENT_SESSIONS = 25;
interface UseNavigationSessionsOptions {
onNavigate?: () => void;
fetchOnMount?: boolean;
export function sortAndTrim(sessions: Session[]): Session[] {
return [...sessions]
.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())
.slice(0, MAX_RECENT_SESSIONS);
}
export function useNavigationSessions(options: UseNavigationSessionsOptions = {}) {
const { onNavigate, fetchOnMount = false } = options;
export function prependUnique(prev: Session[], session: Session): Session[] {
if (prev.some((s) => s.id === session.id)) return prev;
return [session, ...prev].slice(0, MAX_RECENT_SESSIONS);
}
export function useNavigationSessions() {
const navigate = useNavigate();
const location = useLocation();
const [searchParams] = useSearchParams();
const chatContext = useChatContext();
const { extensionsList } = useConfig();
const setView = useNavigation();
const [recentSessions, setRecentSessions] = useState<Session[]>([]);
const sessionsRef = useRef<Session[]>([]);
const lastSessionIdRef = useRef<string | null>(null);
const isCreatingSessionRef = useRef(false);
const activeSessionId = searchParams.get('resumeSessionId') ?? undefined;
const currentSessionId =
location.pathname === '/pair' ? searchParams.get('resumeSessionId') : null;
useEffect(() => {
sessionsRef.current = recentSessions;
}, [recentSessions]);
useEffect(() => {
if (currentSessionId) {
lastSessionIdRef.current = currentSessionId;
@@ -49,33 +42,21 @@ export function useNavigationSessions(options: UseNavigationSessionsOptions = {}
try {
const response = await listSessions({ throwOnError: false });
if (response.data) {
const sorted = [...response.data.sessions]
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
.slice(0, MAX_RECENT_SESSIONS);
setRecentSessions(sorted);
sessionsRef.current = response.data.sessions;
const apiSessions = sortAndTrim(response.data.sessions);
setRecentSessions(apiSessions);
}
} catch (error) {
console.error('Failed to fetch sessions:', error);
}
}, []);
useEffect(() => {
if (fetchOnMount) {
fetchSessions();
}
}, [fetchOnMount, fetchSessions]);
useEffect(() => {
if (!activeSessionId) return;
if (recentSessions.some((s) => s.id === activeSessionId)) return;
getSession({ path: { session_id: activeSessionId }, throwOnError: false }).then((response) => {
if (!response.data) return;
setRecentSessions((prev) => {
if (prev.some((s) => s.id === activeSessionId)) return prev;
return [response.data as Session, ...prev].slice(0, MAX_RECENT_SESSIONS);
});
setRecentSessions((prev) => prependUnique(prev, response.data as Session));
});
}, [activeSessionId, recentSessions]);
@@ -86,11 +67,7 @@ export function useNavigationSessions(options: UseNavigationSessionsOptions = {}
const handleSessionCreated = (event: Event) => {
const { session } = (event as CustomEvent<{ session?: Session }>).detail || {};
if (session) {
setRecentSessions((prev) => {
if (prev.some((s) => s.id === session.id)) return prev;
return [session, ...prev].slice(0, MAX_RECENT_SESSIONS);
});
sessionsRef.current = [session, ...sessionsRef.current.filter((s) => s.id !== session.id)];
setRecentSessions((prev) => prependUnique(prev, session));
}
if (isPolling) return;
@@ -106,15 +83,8 @@ export function useNavigationSessions(options: UseNavigationSessionsOptions = {}
try {
const response = await listSessions({ throwOnError: false });
if (response.data) {
const apiSessions = response.data.sessions.slice(0, MAX_RECENT_SESSIONS);
setRecentSessions((prev) => {
const emptyLocalSessions = prev.filter(
(local) =>
local.message_count === 0 && !apiSessions.some((api) => api.id === local.id)
);
return [...emptyLocalSessions, ...apiSessions].slice(0, MAX_RECENT_SESSIONS);
});
sessionsRef.current = response.data.sessions;
const apiSessions = sortAndTrim(response.data.sessions);
setRecentSessions(apiSessions);
}
} catch (error) {
console.error('Failed to poll sessions:', error);
@@ -145,7 +115,6 @@ export function useNavigationSessions(options: UseNavigationSessionsOptions = {}
const { sessionId } = (event as CustomEvent<{ sessionId: string }>).detail;
setRecentSessions((prev) => prev.filter((session) => session.id !== sessionId));
sessionsRef.current = sessionsRef.current.filter((session) => session.id !== sessionId);
if (lastSessionIdRef.current === sessionId) {
lastSessionIdRef.current = null;
@@ -154,17 +123,8 @@ export function useNavigationSessions(options: UseNavigationSessionsOptions = {}
listSessions({ throwOnError: false })
.then((response) => {
if (version !== fetchVersion || !response.data) return;
const apiSessions = [...response.data.sessions]
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
.slice(0, MAX_RECENT_SESSIONS);
setRecentSessions((prev) => {
const emptyLocalSessions = prev.filter(
(local) =>
local.message_count === 0 && !apiSessions.some((api) => api.id === local.id)
);
return [...emptyLocalSessions, ...apiSessions].slice(0, MAX_RECENT_SESSIONS);
});
sessionsRef.current = response.data.sessions;
const apiSessions = sortAndTrim(response.data.sessions);
setRecentSessions(apiSessions);
})
.catch((error) => console.error('Failed to fetch sessions:', error));
};
@@ -176,9 +136,6 @@ export function useNavigationSessions(options: UseNavigationSessionsOptions = {}
setRecentSessions((prev) =>
prev.map((session) => (session.id === sessionId ? { ...session, name: newName } : session))
);
sessionsRef.current = sessionsRef.current.map((session) =>
session.id === sessionId ? { ...session, name: newName } : session
);
};
window.addEventListener(AppEvents.SESSION_DELETED, handleSessionDeleted);
@@ -203,54 +160,22 @@ export function useNavigationSessions(options: UseNavigationSessionsOptions = {}
} else {
navigate(path);
}
onNavigate?.();
},
[navigate, currentSessionId, chatContext?.chat?.sessionId, onNavigate]
[navigate, currentSessionId, chatContext?.chat?.sessionId]
);
const handleNewChat = useCallback(async () => {
if (isCreatingSessionRef.current) return;
// Only reuse the current window's own active session if it is empty.
// Previously this grabbed the first empty session globally, which caused
// multiple windows to claim the same empty session after a restart/upgrade.
const currentActiveSession = activeSessionId
? sessionsRef.current.find((s) => s.id === activeSessionId)
: undefined;
const canReuseActive = currentActiveSession && shouldShowNewChatTitle(currentActiveSession);
if (canReuseActive) {
resumeSession(currentActiveSession, setView);
} else {
isCreatingSessionRef.current = true;
try {
await startNewSession('', setView, getInitialWorkingDir(), {
allExtensions: extensionsList,
});
} finally {
setTimeout(() => {
isCreatingSessionRef.current = false;
}, 1000);
}
}
onNavigate?.();
}, [setView, onNavigate, extensionsList, activeSessionId]);
const handleSessionClick = useCallback(
(sessionId: string) => {
navigate(`/pair?resumeSessionId=${sessionId}`);
onNavigate?.();
},
[navigate, onNavigate]
[navigate]
);
return {
recentSessions,
activeSessionId,
currentSessionId,
fetchSessions,
handleNavClick,
handleNewChat,
handleSessionClick,
};
}
@@ -267,8 +192,3 @@ export function getSessionDisplayName(session: Session): string {
}
return session.name;
}
export function truncateMessage(msg?: string, maxLen = 20): string {
if (!msg) return 'New Chat';
return msg.length > maxLen ? msg.substring(0, maxLen) + '...' : msg;
}
-12
View File
@@ -155,15 +155,6 @@
"bottomMenuExtensionSelection.searchExtensions": {
"defaultMessage": "search extensions..."
},
"bottomMenuModeSelection.autoFallback": {
"defaultMessage": "auto"
},
"bottomMenuModeSelection.automaticModeDescription": {
"defaultMessage": "Automatic mode selection"
},
"bottomMenuModeSelection.currentModeTitle": {
"defaultMessage": "Current mode: {label} - {description}"
},
"cardButtons.configure": {
"defaultMessage": "Configure"
},
@@ -3809,9 +3800,6 @@
"sessionIndicators.streaming": {
"defaultMessage": "Streaming"
},
"sessionItem.messageCount": {
"defaultMessage": "{count} messages"
},
"sessionSharingSection.alreadyConfigured": {
"defaultMessage": "Session sharing has already been configured"
},
-12
View File
@@ -95,15 +95,6 @@
"bottomMenuExtensionSelection.searchExtensions": {
"defaultMessage": "поиск расширений..."
},
"bottomMenuModeSelection.autoFallback": {
"defaultMessage": "авто"
},
"bottomMenuModeSelection.automaticModeDescription": {
"defaultMessage": "Автоматический выбор режима"
},
"bottomMenuModeSelection.currentModeTitle": {
"defaultMessage": "Текущий режим: {label} - {description}"
},
"cardButtons.configure": {
"defaultMessage": "Настроить"
},
@@ -3728,9 +3719,6 @@
"sessionIndicators.streaming": {
"defaultMessage": "Потоковая передача"
},
"sessionItem.messageCount": {
"defaultMessage": "Сообщений: {count}"
},
"sessionSharingSection.alreadyConfigured": {
"defaultMessage": "Общий доступ к сеансам уже настроен"
},
-12
View File
@@ -98,15 +98,6 @@
"bottomMenuExtensionSelection.searchExtensions": {
"defaultMessage": "uzantıları ara..."
},
"bottomMenuModeSelection.autoFallback": {
"defaultMessage": "otomatik"
},
"bottomMenuModeSelection.automaticModeDescription": {
"defaultMessage": "Otomatik mod seçimi"
},
"bottomMenuModeSelection.currentModeTitle": {
"defaultMessage": "Geçerli mod: {label} - {description}"
},
"cardButtons.configure": {
"defaultMessage": "Yapılandır"
},
@@ -3842,9 +3833,6 @@
"sessionIndicators.streaming": {
"defaultMessage": "Akış"
},
"sessionItem.messageCount": {
"defaultMessage": "{count} mesajları"
},
"sessionSharingSection.alreadyConfigured": {
"defaultMessage": "Oturum paylaşımı zaten yapılandırılmış"
},
-12
View File
@@ -95,15 +95,6 @@
"bottomMenuExtensionSelection.searchExtensions": {
"defaultMessage": "搜索扩展…"
},
"bottomMenuModeSelection.autoFallback": {
"defaultMessage": "自动"
},
"bottomMenuModeSelection.automaticModeDescription": {
"defaultMessage": "自动模式选择"
},
"bottomMenuModeSelection.currentModeTitle": {
"defaultMessage": "当前模式:{label} - {description}"
},
"cardButtons.configure": {
"defaultMessage": "配置"
},
@@ -3662,9 +3653,6 @@
"sessionIndicators.streaming": {
"defaultMessage": "流式传输中"
},
"sessionItem.messageCount": {
"defaultMessage": "{count} 条消息"
},
"sessionSharingSection.alreadyConfigured": {
"defaultMessage": "会话分享已配置"
},