feat: add terminal panel components and Zustand store for state management

- Created a barrel export file for terminal panel components.
- Implemented Zustand store for managing terminal panel UI state, including visibility, active terminal, view mode, and terminal ordering.
- Added actions for opening/closing the terminal panel, setting the active terminal, changing view modes, and managing terminal order.
- Introduced selectors for accessing terminal panel state properties.
This commit is contained in:
catlog22
2026-02-12 23:53:11 +08:00
parent e44a97e812
commit ddbe12b7af
72 changed files with 1055 additions and 254 deletions

View File

@@ -261,7 +261,7 @@ describe('Component Renderer Interface', () => {
});
it('should support async action handlers', async () => {
const asyncAction: ActionHandler = async (actionId, params) => {
const asyncAction: ActionHandler = async (_actionId, _params) => {
await Promise.resolve();
return;
};

View File

@@ -5,8 +5,7 @@
import { describe, it, expect } from 'vitest';
import { z } from 'zod';
import { A2UIParser, a2uiParser, A2UIParseError } from '../core/A2UIParser';
import type { SurfaceUpdate, A2UIComponent } from '../core/A2UITypes';
import { a2uiParser, A2UIParseError } from '../core/A2UIParser';
// Import component renderers to trigger auto-registration
import '../renderer/components';

View File

@@ -4,7 +4,7 @@
// Tests for all A2UI component renderers
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, cleanup, within } from '@testing-library/react';
import { render, screen, cleanup } from '@testing-library/react';
import type { A2UIComponent } from '../core/A2UITypes';
import type { A2UIState, ActionHandler, BindingResolver } from '../core/A2UIComponentRegistry';
import type { TextComponent, ButtonComponent, DropdownComponent, CLIOutputComponent, DateTimeInputComponent } from '../core/A2UITypes';
@@ -653,7 +653,7 @@ describe('A2UI Component Integration', () => {
});
it('should handle async action handlers', async () => {
const asyncOnAction: ActionHandler = async (actionId, params) => {
const asyncOnAction: ActionHandler = async (_actionId, _params) => {
await new Promise((resolve) => setTimeout(resolve, 10));
};

View File

@@ -3,8 +3,8 @@
// ========================================
// React component that renders A2UI surfaces
import React, { useState, useCallback, useMemo } from 'react';
import type { SurfaceUpdate, SurfaceComponent, A2UIComponent, LiteralString, Binding } from '../core/A2UITypes';
import { useState, useCallback } from 'react';
import type { SurfaceUpdate, A2UIComponent, LiteralString, Binding } from '../core/A2UITypes';
import { a2uiRegistry, type A2UIState, type ActionHandler, type BindingResolver } from '../core/A2UIComponentRegistry';
// ========== Renderer Props ==========
@@ -26,7 +26,7 @@ interface A2UIRendererProps {
*/
export function A2UIRenderer({ surface, onAction, className = '' }: A2UIRendererProps) {
// Local state initialized with surface's initial state
const [localState, setLocalState] = useState<A2UIState>(surface.initialState || {});
const [localState] = useState<A2UIState>(surface.initialState || {});
// Handle action from components
const handleAction = useCallback<ActionHandler>(
@@ -57,21 +57,6 @@ export function A2UIRenderer({ surface, onAction, className = '' }: A2UIRenderer
[localState]
);
// Update state from external source
const updateState = useCallback((updates: Partial<A2UIState>) => {
setLocalState((prev) => ({ ...prev, ...updates }));
}, []);
// Memoize context for components
const contextValue = useMemo(
() => ({
state: localState,
resolveBinding,
updateState,
}),
[localState, resolveBinding, updateState]
);
return (
<div className={`a2ui-surface ${className}`} data-surface-id={surface.surfaceId}>
{surface.components.map((comp) => (

View File

@@ -3,25 +3,16 @@
// ========================================
// Maps A2UI Button component to shadcn/ui Button
import React from 'react';
import { Button } from '@/components/ui/Button';
import type { ComponentRenderer } from '../../core/A2UIComponentRegistry';
import type { A2UIState, ActionHandler, BindingResolver } from '../../core/A2UIComponentRegistry';
import type { ButtonComponent, A2UIComponent } from '../../core/A2UITypes';
import type { ButtonComponent } from '../../core/A2UITypes';
import { resolveLiteralOrBinding } from '../A2UIRenderer';
interface A2UIButtonRendererProps {
component: A2UIComponent;
state: A2UIState;
onAction: ActionHandler;
resolveBinding: BindingResolver;
}
/**
* A2UI Button Component Renderer
* Maps A2UI variants (primary/secondary/destructive) to shadcn/ui variants (default/secondary/destructive/ghost)
*/
export const A2UIButton: ComponentRenderer = ({ component, state, onAction, resolveBinding }) => {
export const A2UIButton: ComponentRenderer = ({ component, onAction, resolveBinding }) => {
const buttonComp = component as ButtonComponent;
const { Button: buttonConfig } = buttonComp;

View File

@@ -101,7 +101,7 @@ function StreamingIndicator() {
* A2UI CLIOutput Component Renderer
* Displays CLI output with optional syntax highlighting and streaming indicator
*/
export const A2UICLIOutput: ComponentRenderer = ({ component, state, onAction, resolveBinding }) => {
export const A2UICLIOutput: ComponentRenderer = ({ component, resolveBinding }) => {
const cliOutputComp = component as CLIOutputComponent;
const { CLIOutput: config } = cliOutputComp;

View File

@@ -3,24 +3,16 @@
// ========================================
// Maps A2UI Card component to shadcn/ui Card
import React from 'react';
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/Card';
import type { ComponentRenderer } from '../../core/A2UIComponentRegistry';
import { resolveTextContent } from '../A2UIRenderer';
import type { CardComponent } from '../../core/A2UITypes';
interface A2UICardProps {
component: CardComponent;
state: Record<string, unknown>;
onAction: (actionId: string, params: Record<string, unknown>) => void | Promise<void>;
resolveBinding: (binding: { path: string }) => unknown;
}
/**
* A2UI Card Component Renderer
* Container component with optional title and description
*/
export const A2UICard: ComponentRenderer = ({ component, state, onAction, resolveBinding }) => {
export const A2UICard: ComponentRenderer = ({ component, resolveBinding }) => {
const cardComp = component as CardComponent;
const { Card: cardConfig } = cardComp;

View File

@@ -3,25 +3,18 @@
// ========================================
// Maps A2UI Checkbox component to shadcn/ui Checkbox
import React, { useState, useCallback } from 'react';
import { useState, useCallback } from 'react';
import { Checkbox } from '@/components/ui/Checkbox';
import { Label } from '@/components/ui/Label';
import type { ComponentRenderer } from '../../core/A2UIComponentRegistry';
import { resolveLiteralOrBinding, resolveTextContent } from '../A2UIRenderer';
import type { CheckboxComponent } from '../../core/A2UITypes';
interface A2UICheckboxProps {
component: CheckboxComponent;
state: Record<string, unknown>;
onAction: (actionId: string, params: Record<string, unknown>) => void | Promise<void>;
resolveBinding: (binding: { path: string }) => unknown;
}
/**
* A2UI Checkbox Component Renderer
* Boolean state binding with onChange handler
*/
export const A2UICheckbox: ComponentRenderer = ({ component, state, onAction, resolveBinding }) => {
export const A2UICheckbox: ComponentRenderer = ({ component, onAction, resolveBinding }) => {
const checkboxComp = component as CheckboxComponent;
const { Checkbox: checkboxConfig } = checkboxComp;

View File

@@ -3,9 +3,9 @@
// ========================================
// Date/time picker with ISO string format support
import React, { useState, useCallback, useEffect } from 'react';
import { useState, useCallback, useEffect } from 'react';
import type { ComponentRenderer } from '../../core/A2UIComponentRegistry';
import { resolveLiteralOrBinding, resolveTextContent } from '../A2UIRenderer';
import { resolveTextContent } from '../A2UIRenderer';
import type { DateTimeInputComponent } from '../../core/A2UITypes';
/**
@@ -30,7 +30,7 @@ function isoToDateTimeLocal(isoString: string): string {
/**
* Convert datetime-local input format to ISO string
*/
function dateTimeLocalToIso(dateTimeLocal: string, includeTime: boolean): string {
function dateTimeLocalToIso(dateTimeLocal: string, _includeTime: boolean): string {
if (!dateTimeLocal) return '';
const date = new Date(dateTimeLocal);
@@ -43,7 +43,7 @@ function dateTimeLocalToIso(dateTimeLocal: string, includeTime: boolean): string
* A2UI DateTimeInput Component Renderer
* Uses native input[type="datetime-local"] or input[type="date"] based on includeTime
*/
export const A2UIDateTimeInput: ComponentRenderer = ({ component, state, onAction, resolveBinding }) => {
export const A2UIDateTimeInput: ComponentRenderer = ({ component, onAction, resolveBinding }) => {
const dateTimeComp = component as DateTimeInputComponent;
const { DateTimeInput: config } = dateTimeComp;
const includeTime = config.includeTime ?? true;

View File

@@ -3,24 +3,16 @@
// ========================================
// Maps A2UI Progress component to shadcn/ui Progress
import React from 'react';
import { Progress } from '@/components/ui/Progress';
import type { ComponentRenderer } from '../../core/A2UIComponentRegistry';
import { resolveLiteralOrBinding } from '../A2UIRenderer';
import type { ProgressComponent } from '../../core/A2UITypes';
interface A2UIProgressProps {
component: ProgressComponent;
state: Record<string, unknown>;
onAction: (actionId: string, params: Record<string, unknown>) => void | Promise<void>;
resolveBinding: (binding: { path: string }) => unknown;
}
/**
* A2UI Progress Component Renderer
* For CLI output progress display
*/
export const A2UIProgress: ComponentRenderer = ({ component, state, onAction, resolveBinding }) => {
export const A2UIProgress: ComponentRenderer = ({ component, resolveBinding }) => {
const progressComp = component as ProgressComponent;
const { Progress: progressConfig } = progressComp;

View File

@@ -4,25 +4,18 @@
// Maps A2UI RadioGroup component to shadcn/ui RadioGroup
// Used for single-select questions with visible options
import React, { useState, useCallback } from 'react';
import { useState, useCallback } from 'react';
import { RadioGroup, RadioGroupItem } from '@/components/ui/RadioGroup';
import { Label } from '@/components/ui/Label';
import type { ComponentRenderer } from '../../core/A2UIComponentRegistry';
import { resolveLiteralOrBinding, resolveTextContent } from '../A2UIRenderer';
import type { RadioGroupComponent } from '../../core/A2UITypes';
interface A2UIRadioGroupProps {
component: RadioGroupComponent;
state: Record<string, unknown>;
onAction: (actionId: string, params: Record<string, unknown>) => void | Promise<void>;
resolveBinding: (binding: { path: string }) => unknown;
}
/**
* A2UI RadioGroup Component Renderer
* Single selection from visible options with onChange handler
*/
export const A2UIRadioGroup: ComponentRenderer = ({ component, state, onAction, resolveBinding }) => {
export const A2UIRadioGroup: ComponentRenderer = ({ component, onAction, resolveBinding }) => {
const radioGroupComp = component as RadioGroupComponent;
const { RadioGroup: radioConfig } = radioGroupComp;

View File

@@ -6,20 +6,12 @@
import React from 'react';
import type { ComponentRenderer } from '../../core/A2UIComponentRegistry';
import { resolveTextContent } from '../A2UIRenderer';
import type { TextComponent } from '../../core/A2UITypes';
interface A2UITextProps {
component: TextComponent;
state: Record<string, unknown>;
onAction: (actionId: string, params: Record<string, unknown>) => void | Promise<void>;
resolveBinding: (binding: { path: string }) => unknown;
}
/**
* A2UI Text Component Renderer
* Maps A2UI Text usageHint to HTML elements (h1, h2, h3, p, span, code)
*/
export const A2UIText: ComponentRenderer = ({ component, state, onAction, resolveBinding }) => {
export const A2UIText: ComponentRenderer = ({ component, resolveBinding }) => {
const { Text } = component as { Text: { text: unknown; usageHint?: string } };
// Resolve text content

View File

@@ -3,24 +3,17 @@
// ========================================
// Maps A2UI TextArea component to shadcn/ui Textarea
import React, { useState, useCallback } from 'react';
import { useState, useCallback } from 'react';
import { Textarea } from '@/components/ui/Textarea';
import type { ComponentRenderer } from '../../core/A2UIComponentRegistry';
import { resolveLiteralOrBinding } from '../A2UIRenderer';
import type { TextAreaComponent } from '../../core/A2UITypes';
interface A2UITextAreaProps {
component: TextAreaComponent;
state: Record<string, unknown>;
onAction: (actionId: string, params: Record<string, unknown>) => void | Promise<void>;
resolveBinding: (binding: { path: string }) => unknown;
}
/**
* A2UI TextArea Component Renderer
* Two-way binding via onChange updates to local state
*/
export const A2UITextArea: ComponentRenderer = ({ component, state, onAction, resolveBinding }) => {
export const A2UITextArea: ComponentRenderer = ({ component, onAction, resolveBinding }) => {
const areaComp = component as TextAreaComponent;
const { TextArea: areaConfig } = areaComp;

View File

@@ -3,24 +3,17 @@
// ========================================
// Maps A2UI TextField component to shadcn/ui Input
import React, { useState, useCallback } from 'react';
import { useState, useCallback } from 'react';
import { Input } from '@/components/ui/Input';
import type { ComponentRenderer } from '../../core/A2UIComponentRegistry';
import { resolveLiteralOrBinding } from '../A2UIRenderer';
import type { TextFieldComponent } from '../../core/A2UITypes';
interface A2UITextFieldProps {
component: TextFieldComponent;
state: Record<string, unknown>;
onAction: (actionId: string, params: Record<string, unknown>) => void | Promise<void>;
resolveBinding: (binding: { path: string }) => unknown;
}
/**
* A2UI TextField Component Renderer
* Two-way binding via onChange updates to local state
*/
export const A2UITextField: ComponentRenderer = ({ component, state, onAction, resolveBinding }) => {
export const A2UITextField: ComponentRenderer = ({ component, onAction, resolveBinding }) => {
const fieldComp = component as TextFieldComponent;
const { TextField: fieldConfig } = fieldComp;