mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
code refactor
This commit is contained in:
@@ -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,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user