mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
refactor: remove dead component and useNavigationSessions cleanup (#9603)
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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": "Общий доступ к сеансам уже настроен"
|
||||
},
|
||||
|
||||
@@ -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ış"
|
||||
},
|
||||
|
||||
@@ -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": "会话分享已配置"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user