feat: Implement phases 6 to 9 of the review cycle fix process, including discovery, batching, parallel planning, execution, and completion

- Added Phase 6: Fix Discovery & Batching with intelligent grouping and batching of findings.
- Added Phase 7: Fix Parallel Planning to launch planning agents for concurrent analysis and aggregation of partial plans.
- Added Phase 8: Fix Execution for stage-based execution of fixes with conservative test verification.
- Added Phase 9: Fix Completion to aggregate results, generate summary reports, and handle session completion.
- Introduced new frontend components: ResizeHandle for draggable resizing of sidebar panels and useResizablePanel hook for managing panel sizes with localStorage persistence.
- Added PowerShell script for checking TypeScript errors in source code, excluding test files.
This commit is contained in:
catlog22
2026-02-07 19:28:33 +08:00
parent ba5f4eba84
commit d43696d756
90 changed files with 8462 additions and 616 deletions

View File

@@ -5,7 +5,8 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { A2UIComponentRegistry, a2uiRegistry } from '../core/A2UIComponentRegistry';
import type { A2UIComponent, A2UIState, ActionHandler, BindingResolver } from '../core/A2UIComponentRegistry';
import type { A2UIState, ActionHandler, BindingResolver } from '../core/A2UIComponentRegistry';
import type { A2UIComponent } from '../core/A2UITypes';
// Import component renderers to trigger auto-registration
import '../renderer/components';
@@ -24,15 +25,15 @@ describe('A2UIComponentRegistry', () => {
describe('register()', () => {
it('should register a component renderer', () => {
registry.register('TestComponent', mockRenderer);
expect(registry.has('TestComponent')).toBe(true);
registry.register('TestComponent' as any, mockRenderer);
expect(registry.has('TestComponent' as any)).toBe(true);
});
it('should allow overriding existing renderer', () => {
registry.register('TestComponent', mockRenderer);
registry.register('TestComponent', anotherMockRenderer);
const retrieved = registry.get('TestComponent');
registry.register('TestComponent' as any, mockRenderer);
registry.register('TestComponent' as any, anotherMockRenderer);
const retrieved = registry.get('TestComponent' as any);
expect(retrieved).toBe(anotherMockRenderer);
});
@@ -49,57 +50,57 @@ describe('A2UIComponentRegistry', () => {
describe('unregister()', () => {
it('should remove a registered renderer', () => {
registry.register('TestComponent', mockRenderer);
expect(registry.has('TestComponent')).toBe(true);
registry.register('TestComponent' as any, mockRenderer);
expect(registry.has('TestComponent' as any)).toBe(true);
registry.unregister('TestComponent');
expect(registry.has('TestComponent')).toBe(false);
registry.unregister('TestComponent' as any);
expect(registry.has('TestComponent' as any)).toBe(false);
});
it('should be idempotent for non-existent components', () => {
expect(() => registry.unregister('NonExistent')).not.toThrow();
expect(registry.has('NonExistent')).toBe(false);
expect(() => registry.unregister('NonExistent' as any)).not.toThrow();
expect(registry.has('NonExistent' as any)).toBe(false);
});
});
describe('get()', () => {
it('should return registered renderer', () => {
registry.register('TestComponent', mockRenderer);
const retrieved = registry.get('TestComponent');
registry.register('TestComponent' as any, mockRenderer);
const retrieved = registry.get('TestComponent' as any);
expect(retrieved).toBe(mockRenderer);
});
it('should return undefined for unregistered component', () => {
const retrieved = registry.get('NonExistent');
const retrieved = registry.get('NonExistent' as any);
expect(retrieved).toBeUndefined();
});
it('should return correct renderer after multiple registrations', () => {
registry.register('First', mockRenderer);
registry.register('Second', anotherMockRenderer);
registry.register('First' as any, mockRenderer);
registry.register('Second' as any, anotherMockRenderer);
expect(registry.get('First')).toBe(mockRenderer);
expect(registry.get('Second')).toBe(anotherMockRenderer);
expect(registry.get('First' as any)).toBe(mockRenderer);
expect(registry.get('Second' as any)).toBe(anotherMockRenderer);
});
});
describe('has()', () => {
it('should return true for registered components', () => {
registry.register('TestComponent', mockRenderer);
expect(registry.has('TestComponent')).toBe(true);
registry.register('TestComponent' as any, mockRenderer);
expect(registry.has('TestComponent' as any)).toBe(true);
});
it('should return false for unregistered components', () => {
expect(registry.has('NonExistent')).toBe(false);
expect(registry.has('NonExistent' as any)).toBe(false);
});
it('should return false after unregistering', () => {
registry.register('TestComponent', mockRenderer);
expect(registry.has('TestComponent')).toBe(true);
registry.register('TestComponent' as any, mockRenderer);
expect(registry.has('TestComponent' as any)).toBe(true);
registry.unregister('TestComponent');
expect(registry.has('TestComponent')).toBe(false);
registry.unregister('TestComponent' as any);
expect(registry.has('TestComponent' as any)).toBe(false);
});
});
@@ -149,7 +150,7 @@ describe('A2UIComponentRegistry', () => {
});
it('should be idempotent', () => {
registry.register('Test', mockRenderer);
registry.register('Test' as any, mockRenderer);
registry.clear();
expect(registry.size).toBe(0);

View File

@@ -376,15 +376,13 @@ describe('A2UIParser', () => {
code: z.ZodIssueCode.invalid_type,
path: ['components', 0, 'id'],
expected: 'string',
received: 'undefined',
message: 'Required',
},
} as any,
{
code: z.ZodIssueCode.invalid_string,
code: 'invalid_format' as any,
path: ['surfaceId'],
validation: 'uuid',
message: 'Invalid format',
},
} as any,
]);
const parseError = new A2UIParseError('Validation failed', zodError);

View File

@@ -105,14 +105,14 @@ export class A2UIParser {
* @param json - JSON string to parse
* @returns Result object with success flag and data or error
*/
safeParse(json: string): z.SafeParseReturnType<SurfaceUpdate, SurfaceUpdate> {
safeParse(json: string): ReturnType<typeof SurfaceUpdateSchema.safeParse> {
try {
const data = JSON.parse(json);
return SurfaceUpdateSchema.safeParse(data);
} catch (error) {
return {
success: false as const,
error: error as z.ZodError,
error: error as any,
};
}
}
@@ -122,7 +122,7 @@ export class A2UIParser {
* @param data - Object to validate
* @returns Result object with success flag and data or error
*/
safeParseObject(data: unknown): z.SafeParseReturnType<SurfaceUpdate, SurfaceUpdate> {
safeParseObject(data: unknown): ReturnType<typeof SurfaceUpdateSchema.safeParse> {
return SurfaceUpdateSchema.safeParse(data);
}

View File

@@ -41,7 +41,7 @@ export const BooleanContentSchema = z.union([
/** Action trigger */
export const ActionSchema = z.object({
actionId: z.string(),
parameters: z.record(z.unknown()).optional(),
parameters: z.record(z.string(), z.unknown()).optional(),
});
/** Text component */
@@ -196,7 +196,7 @@ export const DisplayModeSchema = z.enum(['popup', 'panel']);
export const SurfaceUpdateSchema = z.object({
surfaceId: z.string(),
components: z.array(SurfaceComponentSchema),
initialState: z.record(z.unknown()).optional(),
initialState: z.record(z.string(), z.unknown()).optional(),
/** Display mode: 'popup' for centered dialog, 'panel' for notification panel */
displayMode: DisplayModeSchema.optional(),
});

View File

@@ -162,7 +162,7 @@ export function resolveLiteralOrBinding(
const value = resolveBinding(content as Binding);
// Return resolved value or empty string as fallback
return value ?? '';
return (value ?? '') as string | number | boolean;
}
/**

View File

@@ -16,7 +16,7 @@ function highlightSyntax(output: string, language?: string): React.ReactNode {
const lines = output.split('\n');
// Define syntax patterns by language
const patterns: Record<string, RegExp[]> = {
const patterns: Record<string, { regex: RegExp; className: string }[]> = {
bash: [
{ regex: /^(\$|>|\s)(\s*)/gm, className: 'text-muted-foreground' }, // Prompt
{ regex: /\b(error|fail|failed|failure)\b/gi, className: 'text-destructive font-semibold' },
@@ -55,8 +55,8 @@ function highlightSyntax(output: string, language?: string): React.ReactNode {
for (const pattern of langPatterns) {
if (typeof result === 'string') {
const parts = result.split(pattern.regex);
result = parts.map((part, i) => {
const parts: string[] = result.split(pattern.regex);
result = parts.map((part: string, i: number) => {
if (pattern.regex.test(part)) {
return (
<span key={`${key}-${i}`} className={pattern.className}>

View File

@@ -23,7 +23,7 @@ export const A2UIText: ComponentRenderer = ({ component, state, onAction, resolv
const { Text } = component as { Text: { text: unknown; usageHint?: string } };
// Resolve text content
const text = resolveTextContent(Text.text, resolveBinding);
const text = resolveTextContent(Text.text as { literalString: string } | { path: string }, resolveBinding);
const usageHint = Text.usageHint || 'span';
// Map usageHint to HTML elements