Merge branch 'main' into fix/pr2371-btw-complete

This commit is contained in:
yiliang114 2026-03-21 01:10:48 +08:00
commit 905f2c3f36
108 changed files with 5064 additions and 965 deletions

View file

@ -268,7 +268,7 @@ describe('AppContainer State Management', () => {
// Mock config's getTargetDir to return consistent workspace directory
vi.spyOn(mockConfig, 'getTargetDir').mockReturnValue('/test/workspace');
// Mock GeminiClient to prevent unhandled errors from TaskTool.refreshSubagents
// Mock GeminiClient to prevent unhandled errors from AgentTool.refreshSubagents
const mockGeminiClient: Partial<GeminiClient> = {
initialize: vi.fn().mockResolvedValue(undefined),
setTools: vi.fn().mockResolvedValue(undefined),
@ -278,7 +278,7 @@ describe('AppContainer State Management', () => {
mockGeminiClient as GeminiClient,
);
// Mock SubagentManager to prevent errors during TaskTool initialization
// Mock SubagentManager to prevent errors during AgentTool initialization
const mockSubagentManager: Partial<SubagentManager> = {
listSubagents: vi.fn().mockResolvedValue([]),
addChangeListener: vi.fn(),

View file

@ -0,0 +1,66 @@
/**
* @license
* Copyright 2025 Qwen Code
* SPDX-License-Identifier: Apache-2.0
*/
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
import path from 'path';
import open from 'open';
import { Storage } from '@qwen-code/qwen-code-core';
import { insightCommand } from './insightCommand.js';
import type { CommandContext } from './types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
const mockGenerateStaticInsight = vi.fn();
vi.mock('../../services/insight/generators/StaticInsightGenerator.js', () => ({
StaticInsightGenerator: vi.fn(() => ({
generateStaticInsight: mockGenerateStaticInsight,
})),
}));
vi.mock('open', () => ({
default: vi.fn(),
}));
describe('insightCommand', () => {
let mockContext: CommandContext;
beforeEach(() => {
Storage.setRuntimeBaseDir(path.resolve('runtime-output'));
mockGenerateStaticInsight.mockResolvedValue(
path.resolve('runtime-output', 'insights', 'insight-2026-03-05.html'),
);
vi.mocked(open).mockResolvedValue(undefined as never);
mockContext = createMockCommandContext({
services: {
config: {} as CommandContext['services']['config'],
},
ui: {
addItem: vi.fn(),
setPendingItem: vi.fn(),
setDebugMessage: vi.fn(),
},
} as unknown as CommandContext);
});
afterEach(() => {
Storage.setRuntimeBaseDir(null);
vi.restoreAllMocks();
});
it('uses runtime base dir to locate projects directory', async () => {
if (!insightCommand.action) {
throw new Error('insight command must have action');
}
await insightCommand.action(mockContext, '');
expect(mockGenerateStaticInsight).toHaveBeenCalledWith(
path.join(Storage.getRuntimeBaseDir(), 'projects'),
expect.any(Function),
);
});
});

View file

@ -10,9 +10,8 @@ import { MessageType } from '../types.js';
import type { HistoryItemInsightProgress } from '../types.js';
import { t } from '../../i18n/index.js';
import { join } from 'path';
import os from 'os';
import { StaticInsightGenerator } from '../../services/insight/generators/StaticInsightGenerator.js';
import { createDebugLogger } from '@qwen-code/qwen-code-core';
import { createDebugLogger, Storage } from '@qwen-code/qwen-code-core';
import open from 'open';
const logger = createDebugLogger('DataProcessor');
@ -29,7 +28,7 @@ export const insightCommand: SlashCommand = {
try {
context.ui.setDebugMessage(t('Generating insights...'));
const projectsDir = join(os.homedir(), '.qwen', 'projects');
const projectsDir = join(Storage.getRuntimeBaseDir(), 'projects');
if (!context.services.config) {
throw new Error('Config service is not available');
}

View file

@ -16,7 +16,7 @@ import { MaxSizedBox } from '../shared/MaxSizedBox.js';
import { TodoDisplay } from '../TodoDisplay.js';
import type {
TodoResultDisplay,
TaskResultDisplay,
AgentResultDisplay,
PlanResultDisplay,
AnsiOutput,
Config,
@ -50,7 +50,7 @@ type DisplayRendererResult =
| { type: 'plan'; data: PlanResultDisplay }
| { type: 'string'; data: string }
| { type: 'diff'; data: { fileDiff: string; fileName: string } }
| { type: 'task'; data: TaskResultDisplay }
| { type: 'task'; data: AgentResultDisplay }
| { type: 'ansi'; data: AnsiOutput };
/**
@ -98,7 +98,7 @@ const useResultDisplayRenderer = (
) {
return {
type: 'task',
data: resultDisplay as TaskResultDisplay,
data: resultDisplay as AgentResultDisplay,
};
}
@ -169,7 +169,7 @@ const PlanResultRenderer: React.FC<{
* Component to render subagent execution results
*/
const SubagentExecutionRenderer: React.FC<{
data: TaskResultDisplay;
data: AgentResultDisplay;
availableHeight?: number;
childWidth: number;
config: Config;

View file

@ -7,7 +7,7 @@
import React, { useMemo } from 'react';
import { Box, Text } from 'ink';
import type {
TaskResultDisplay,
AgentResultDisplay,
AgentStatsSummary,
Config,
} from '@qwen-code/qwen-code-core';
@ -20,7 +20,7 @@ import { ToolConfirmationMessage } from '../../messages/ToolConfirmationMessage.
export type DisplayMode = 'compact' | 'default' | 'verbose';
export interface AgentExecutionDisplayProps {
data: TaskResultDisplay;
data: AgentResultDisplay;
availableHeight?: number;
childWidth: number;
config: Config;
@ -28,7 +28,7 @@ export interface AgentExecutionDisplayProps {
const getStatusColor = (
status:
| TaskResultDisplay['status']
| AgentResultDisplay['status']
| 'executing'
| 'success'
| 'awaiting_approval',
@ -50,7 +50,7 @@ const getStatusColor = (
}
};
const getStatusText = (status: TaskResultDisplay['status']) => {
const getStatusText = (status: AgentResultDisplay['status']) => {
switch (status) {
case 'running':
return 'Running';
@ -301,7 +301,7 @@ const TaskPromptSection: React.FC<{
* Status dot component with similar height as text
*/
const StatusDot: React.FC<{
status: TaskResultDisplay['status'];
status: AgentResultDisplay['status'];
}> = ({ status }) => (
<Box marginLeft={1} marginRight={1}>
<Text color={getStatusColor(status)}></Text>
@ -312,7 +312,7 @@ const StatusDot: React.FC<{
* Status indicator component
*/
const StatusIndicator: React.FC<{
status: TaskResultDisplay['status'];
status: AgentResultDisplay['status'];
}> = ({ status }) => {
const color = getStatusColor(status);
const text = getStatusText(status);
@ -323,7 +323,7 @@ const StatusIndicator: React.FC<{
* Tool calls list - format consistent with ToolInfo in ToolMessage.tsx
*/
const ToolCallsList: React.FC<{
toolCalls: TaskResultDisplay['toolCalls'];
toolCalls: AgentResultDisplay['toolCalls'];
displayMode: DisplayMode;
}> = ({ toolCalls, displayMode }) => {
const calls = toolCalls || [];
@ -435,7 +435,7 @@ const ToolCallItem: React.FC<{
* Execution summary details component
*/
const ExecutionSummaryDetails: React.FC<{
data: TaskResultDisplay;
data: AgentResultDisplay;
displayMode: DisplayMode;
}> = ({ data, displayMode: _displayMode }) => {
const stats = data.executionSummary;
@ -505,7 +505,7 @@ const ToolUsageStats: React.FC<{
* Results section for completed executions - matches the clean layout from the image
*/
const ResultsSection: React.FC<{
data: TaskResultDisplay;
data: AgentResultDisplay;
displayMode: DisplayMode;
}> = ({ data, displayMode }) => (
<Box flexDirection="column" gap={1}>

View file

@ -181,10 +181,7 @@ function calculateFileStats(records: ChatRecord[]): FileOperationStats {
let filePath: string;
if (typeof display.fileName === 'string') {
// Prefer args.file_path for full path, fallback to fileName (which may be basename)
filePath =
(args?.['file_path'] as string) ||
(args?.['absolute_path'] as string) ||
display.fileName;
filePath = (args?.['file_path'] as string) || display.fileName;
} else {
// Fallback if fileName is not a string
filePath = 'unknown';

View file

@ -100,6 +100,10 @@ function formatToolDescription(
const invocation = tool.build(args);
return invocation.getDescription();
} catch {
// Fallback: use the description arg directly if available
if (typeof args['description'] === 'string') {
return args['description'];
}
return '';
}
}