code refactor

This commit is contained in:
tanzhenxin
2025-12-16 20:03:49 +08:00
parent 2837aa6b7c
commit fb8412a96a
7 changed files with 200 additions and 173 deletions

View File

@@ -99,7 +99,6 @@ import { processVisionSwitchOutcome } from './hooks/useVisionAutoSwitch.js';
import { useSubagentCreateDialog } from './hooks/useSubagentCreateDialog.js'; import { useSubagentCreateDialog } from './hooks/useSubagentCreateDialog.js';
import { useAgentsManagerDialog } from './hooks/useAgentsManagerDialog.js'; import { useAgentsManagerDialog } from './hooks/useAgentsManagerDialog.js';
import { useAttentionNotifications } from './hooks/useAttentionNotifications.js'; import { useAttentionNotifications } from './hooks/useAttentionNotifications.js';
import { useSessionSelect } from './hooks/useSessionSelect.js';
const CTRL_EXIT_PROMPT_DURATION_MS = 1000; const CTRL_EXIT_PROMPT_DURATION_MS = 1000;
@@ -437,13 +436,14 @@ export const AppContainer = (props: AppContainerProps) => {
const { isModelDialogOpen, openModelDialog, closeModelDialog } = const { isModelDialogOpen, openModelDialog, closeModelDialog } =
useModelCommand(); useModelCommand();
const { isResumeDialogOpen, openResumeDialog, closeResumeDialog } = const {
useResumeCommand(); isResumeDialogOpen,
openResumeDialog,
const handleResumeSessionSelect = useSessionSelect({ closeResumeDialog,
handleResume,
} = useResumeCommand({
config, config,
historyManager, historyManager,
closeResumeDialog,
startNewSession, startNewSession,
remount: refreshStatic, remount: refreshStatic,
}); });
@@ -1442,7 +1442,7 @@ export const AppContainer = (props: AppContainerProps) => {
// Resume session dialog // Resume session dialog
openResumeDialog, openResumeDialog,
closeResumeDialog, closeResumeDialog,
handleResumeSessionSelect, handleResume,
}), }),
[ [
handleThemeSelect, handleThemeSelect,
@@ -1478,7 +1478,7 @@ export const AppContainer = (props: AppContainerProps) => {
// Resume session dialog // Resume session dialog
openResumeDialog, openResumeDialog,
closeResumeDialog, closeResumeDialog,
handleResumeSessionSelect, handleResume,
], ],
); );

View File

@@ -296,7 +296,7 @@ export const DialogManager = ({
<SessionPicker <SessionPicker
sessionService={config.getSessionService()} sessionService={config.getSessionService()}
currentBranch={getGitBranch(config.getTargetDir())} currentBranch={getGitBranch(config.getTargetDir())}
onSelect={uiActions.handleResumeSessionSelect} onSelect={uiActions.handleResume}
onCancel={uiActions.closeResumeDialog} onCancel={uiActions.closeResumeDialog}
/> />
); );

View File

@@ -67,7 +67,7 @@ export interface UIActions {
// Resume session dialog // Resume session dialog
openResumeDialog: () => void; openResumeDialog: () => void;
closeResumeDialog: () => void; closeResumeDialog: () => void;
handleResumeSessionSelect: (sessionId: string) => void; handleResume: (sessionId: string) => void;
} }
export const UIActionsContext = createContext<UIActions | null>(null); export const UIActionsContext = createContext<UIActions | null>(null);

View File

@@ -5,9 +5,59 @@
*/ */
import { act, renderHook } from '@testing-library/react'; import { act, renderHook } from '@testing-library/react';
import { describe, it, expect } from 'vitest'; import { describe, it, expect, vi } from 'vitest';
import { useResumeCommand } from './useResumeCommand.js'; import { useResumeCommand } from './useResumeCommand.js';
const resumeMocks = vi.hoisted(() => {
let resolveLoadSession:
| ((value: { conversation: unknown } | undefined) => void)
| undefined;
let pendingLoadSession:
| Promise<{ conversation: unknown } | undefined>
| undefined;
return {
createPendingLoadSession() {
pendingLoadSession = new Promise((resolve) => {
resolveLoadSession = resolve;
});
return pendingLoadSession;
},
resolvePendingLoadSession(value: { conversation: unknown } | undefined) {
resolveLoadSession?.(value);
},
getPendingLoadSession() {
return pendingLoadSession;
},
reset() {
resolveLoadSession = undefined;
pendingLoadSession = undefined;
},
};
});
vi.mock('../utils/resumeHistoryUtils.js', () => ({
buildResumedHistoryItems: vi.fn(() => [{ id: 1, type: 'user', text: 'hi' }]),
}));
vi.mock('@qwen-code/qwen-code-core', () => {
class SessionService {
constructor(_cwd: string) {}
async loadSession(_sessionId: string) {
return (
resumeMocks.getPendingLoadSession() ??
Promise.resolve({
conversation: [{ role: 'user', parts: [{ text: 'hello' }] }],
})
);
}
}
return {
SessionService,
};
});
describe('useResumeCommand', () => { describe('useResumeCommand', () => {
it('should initialize with dialog closed', () => { it('should initialize with dialog closed', () => {
const { result } = renderHook(() => useResumeCommand()); const { result } = renderHook(() => useResumeCommand());
@@ -48,10 +98,91 @@ describe('useResumeCommand', () => {
const initialOpenFn = result.current.openResumeDialog; const initialOpenFn = result.current.openResumeDialog;
const initialCloseFn = result.current.closeResumeDialog; const initialCloseFn = result.current.closeResumeDialog;
const initialHandleResume = result.current.handleResume;
rerender(); rerender();
expect(result.current.openResumeDialog).toBe(initialOpenFn); expect(result.current.openResumeDialog).toBe(initialOpenFn);
expect(result.current.closeResumeDialog).toBe(initialCloseFn); expect(result.current.closeResumeDialog).toBe(initialCloseFn);
expect(result.current.handleResume).toBe(initialHandleResume);
});
it('handleResume no-ops when config is null', async () => {
const historyManager = { clearItems: vi.fn(), loadHistory: vi.fn() };
const startNewSession = vi.fn();
const { result } = renderHook(() =>
useResumeCommand({
config: null,
historyManager,
startNewSession,
}),
);
await act(async () => {
await result.current.handleResume('session-1');
});
expect(startNewSession).not.toHaveBeenCalled();
expect(historyManager.clearItems).not.toHaveBeenCalled();
expect(historyManager.loadHistory).not.toHaveBeenCalled();
});
it('handleResume closes the dialog immediately and restores session state', async () => {
resumeMocks.reset();
resumeMocks.createPendingLoadSession();
const historyManager = { clearItems: vi.fn(), loadHistory: vi.fn() };
const startNewSession = vi.fn();
const geminiClient = {
initialize: vi.fn(),
};
const config = {
getTargetDir: () => '/tmp',
getGeminiClient: () => geminiClient,
startNewSession: vi.fn(),
} as unknown as import('@qwen-code/qwen-code-core').Config;
const { result } = renderHook(() =>
useResumeCommand({
config,
historyManager,
startNewSession,
}),
);
// Open first so we can verify the dialog closes immediately.
act(() => {
result.current.openResumeDialog();
});
expect(result.current.isResumeDialogOpen).toBe(true);
const resumePromise = act(async () => {
// Intentionally do not resolve loadSession yet.
await result.current.handleResume('session-2');
});
// After the first flush, the dialog should already be closed even though
// the session load is still pending.
await act(async () => {});
expect(result.current.isResumeDialogOpen).toBe(false);
// Now finish the async load and let the handler complete.
resumeMocks.resolvePendingLoadSession({
conversation: [{ role: 'user', parts: [{ text: 'hello' }] }],
});
await resumePromise;
expect(config.startNewSession).toHaveBeenCalledWith(
'session-2',
expect.objectContaining({
conversation: expect.anything(),
}),
);
expect(startNewSession).toHaveBeenCalledWith('session-2');
expect(geminiClient.initialize).toHaveBeenCalledTimes(1);
expect(historyManager.clearItems).toHaveBeenCalledTimes(1);
expect(historyManager.loadHistory).toHaveBeenCalledTimes(1);
}); });
}); });

View File

@@ -5,8 +5,27 @@
*/ */
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { SessionService, type Config } from '@qwen-code/qwen-code-core';
import { buildResumedHistoryItems } from '../utils/resumeHistoryUtils.js';
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
export function useResumeCommand() { export interface UseResumeCommandOptions {
config: Config | null;
historyManager: Pick<UseHistoryManagerReturn, 'clearItems' | 'loadHistory'>;
startNewSession: (sessionId: string) => void;
remount?: () => void;
}
export interface UseResumeCommandResult {
isResumeDialogOpen: boolean;
openResumeDialog: () => void;
closeResumeDialog: () => void;
handleResume: (sessionId: string) => void;
}
export function useResumeCommand(
options?: UseResumeCommandOptions,
): UseResumeCommandResult {
const [isResumeDialogOpen, setIsResumeDialogOpen] = useState(false); const [isResumeDialogOpen, setIsResumeDialogOpen] = useState(false);
const openResumeDialog = useCallback(() => { const openResumeDialog = useCallback(() => {
@@ -17,9 +36,47 @@ export function useResumeCommand() {
setIsResumeDialogOpen(false); setIsResumeDialogOpen(false);
}, []); }, []);
const { config, historyManager, startNewSession, remount } = options ?? {};
const handleResume = useCallback(
async (sessionId: string) => {
if (!config || !historyManager || !startNewSession) {
return;
}
// Close dialog immediately to prevent input capture during async operations.
closeResumeDialog();
const cwd = config.getTargetDir();
const sessionService = new SessionService(cwd);
const sessionData = await sessionService.loadSession(sessionId);
if (!sessionData) {
return;
}
// Start new session in UI context.
startNewSession(sessionId);
// Reset UI history.
const uiHistoryItems = buildResumedHistoryItems(sessionData, config);
historyManager.clearItems();
historyManager.loadHistory(uiHistoryItems);
// Update session history core.
config.startNewSession(sessionId, sessionData);
await config.getGeminiClient()?.initialize?.();
// Refresh terminal UI.
remount?.();
},
[closeResumeDialog, config, historyManager, startNewSession, remount],
);
return { return {
isResumeDialogOpen, isResumeDialogOpen,
openResumeDialog, openResumeDialog,
closeResumeDialog, closeResumeDialog,
handleResume,
}; };
} }

View File

@@ -1,97 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Code
* SPDX-License-Identifier: Apache-2.0
*/
import { act, renderHook } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { useSessionSelect } from './useSessionSelect.js';
vi.mock('../utils/resumeHistoryUtils.js', () => ({
buildResumedHistoryItems: vi.fn(() => [{ id: 1, type: 'user', text: 'hi' }]),
}));
vi.mock('@qwen-code/qwen-code-core', () => {
class SessionService {
constructor(_cwd: string) {}
async loadSession(_sessionId: string) {
return { conversation: [{ role: 'user', parts: [{ text: 'hello' }] }] };
}
}
return {
SessionService,
buildApiHistoryFromConversation: vi.fn(() => [{ role: 'user', parts: [] }]),
replayUiTelemetryFromConversation: vi.fn(),
uiTelemetryService: { reset: vi.fn() },
};
});
describe('useSessionSelect', () => {
it('no-ops when config is null', async () => {
const closeResumeDialog = vi.fn();
const historyManager = { clearItems: vi.fn(), loadHistory: vi.fn() };
const startNewSession = vi.fn();
const { result } = renderHook(() =>
useSessionSelect({
config: null,
closeResumeDialog,
historyManager,
startNewSession,
}),
);
await act(async () => {
await result.current('session-1');
});
expect(closeResumeDialog).not.toHaveBeenCalled();
expect(startNewSession).not.toHaveBeenCalled();
expect(historyManager.clearItems).not.toHaveBeenCalled();
expect(historyManager.loadHistory).not.toHaveBeenCalled();
});
it('closes the dialog immediately and restores session state', async () => {
const closeResumeDialog = vi.fn();
const historyManager = { clearItems: vi.fn(), loadHistory: vi.fn() };
const startNewSession = vi.fn();
const geminiClient = {
initialize: vi.fn(),
};
const config = {
getTargetDir: () => '/tmp',
getGeminiClient: () => geminiClient,
startNewSession: vi.fn(),
} as unknown as import('@qwen-code/qwen-code-core').Config;
const { result } = renderHook(() =>
useSessionSelect({
config,
closeResumeDialog,
historyManager,
startNewSession,
}),
);
const resumePromise = act(async () => {
await result.current('session-2');
});
expect(closeResumeDialog).toHaveBeenCalledTimes(1);
await resumePromise;
expect(config.startNewSession).toHaveBeenCalledWith(
'session-2',
expect.objectContaining({
conversation: expect.anything(),
}),
);
expect(startNewSession).toHaveBeenCalledWith('session-2');
expect(geminiClient.initialize).toHaveBeenCalledTimes(1);
expect(historyManager.clearItems).toHaveBeenCalledTimes(1);
expect(historyManager.loadHistory).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,64 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Code
* SPDX-License-Identifier: Apache-2.0
*/
import { useCallback } from 'react';
import { SessionService, type Config } from '@qwen-code/qwen-code-core';
import { buildResumedHistoryItems } from '../utils/resumeHistoryUtils.js';
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
export interface UseSessionSelectOptions {
config: Config | null;
historyManager: Pick<UseHistoryManagerReturn, 'clearItems' | 'loadHistory'>;
closeResumeDialog: () => void;
startNewSession: (sessionId: string) => void;
remount?: () => void;
}
/**
* Returns a stable callback to resume a saved session and restore UI + client state.
*/
export function useSessionSelect({
config,
closeResumeDialog,
historyManager,
startNewSession,
remount,
}: UseSessionSelectOptions): (sessionId: string) => void {
return useCallback(
async (sessionId: string) => {
if (!config) {
return;
}
// Close dialog immediately to prevent input capture during async operations.
closeResumeDialog();
const cwd = config.getTargetDir();
const sessionService = new SessionService(cwd);
const sessionData = await sessionService.loadSession(sessionId);
if (!sessionData) {
return;
}
// Start new session in UI context.
startNewSession(sessionId);
// Reset UI history.
const uiHistoryItems = buildResumedHistoryItems(sessionData, config);
historyManager.clearItems();
historyManager.loadHistory(uiHistoryItems);
// Update session history core.
config.startNewSession(sessionId, sessionData);
await config.getGeminiClient()?.initialize?.();
// Refresh terminal UI.
remount?.();
},
[closeResumeDialog, config, historyManager, startNewSession, remount],
);
}