feat(a2ui): Implement A2UI backend with question handling and WebSocket support

- Added A2UITypes for defining question structures and answers.
- Created A2UIWebSocketHandler for managing WebSocket connections and message handling.
- Developed ask-question tool for interactive user questions via A2UI.
- Introduced platformUtils for platform detection and shell command handling.
- Centralized TypeScript types in index.ts for better organization.
- Implemented compatibility checks for hook templates based on platform requirements.
This commit is contained in:
catlog22
2026-01-31 15:27:12 +08:00
parent 4e009bb03a
commit 715ef12c92
163 changed files with 19495 additions and 715 deletions

View File

@@ -8,6 +8,10 @@ import { cn } from '@/lib/utils';
import { Header } from './Header';
import { Sidebar } from './Sidebar';
import { MainContent } from './MainContent';
import { CliStreamMonitor } from '@/components/shared/CliStreamMonitor';
import { NotificationPanel } from '@/components/notification';
import { useNotificationStore } from '@/stores';
import { useWebSocketNotifications } from '@/hooks';
export interface AppShellProps {
/** Initial sidebar collapsed state */
@@ -44,6 +48,23 @@ export function AppShell({
// Mobile sidebar open state
const [mobileOpen, setMobileOpen] = useState(false);
// CLI Monitor open state
const [isCliMonitorOpen, setIsCliMonitorOpen] = useState(false);
// Notification panel store integration
const isNotificationPanelVisible = useNotificationStore((state) => state.isPanelVisible);
const loadPersistentNotifications = useNotificationStore(
(state) => state.loadPersistentNotifications
);
// Initialize WebSocket notifications handler
useWebSocketNotifications();
// Load persistent notifications from localStorage on mount
useEffect(() => {
loadPersistentNotifications();
}, [loadPersistentNotifications]);
// Persist sidebar state
useEffect(() => {
localStorage.setItem(SIDEBAR_COLLAPSED_KEY, JSON.stringify(sidebarCollapsed));
@@ -73,6 +94,18 @@ export function AppShell({
setSidebarCollapsed(collapsed);
}, []);
const handleCliMonitorClick = useCallback(() => {
setIsCliMonitorOpen(true);
}, []);
const handleCliMonitorClose = useCallback(() => {
setIsCliMonitorOpen(false);
}, []);
const handleNotificationPanelClose = useCallback(() => {
useNotificationStore.getState().setPanelVisible(false);
}, []);
return (
<div className="flex flex-col min-h-screen bg-background">
{/* Header - fixed at top */}
@@ -81,6 +114,7 @@ export function AppShell({
projectPath={projectPath}
onRefresh={onRefresh}
isRefreshing={isRefreshing}
onCliMonitorClick={handleCliMonitorClick}
/>
{/* Main layout - sidebar + content */}
@@ -97,13 +131,25 @@ export function AppShell({
<MainContent
className={cn(
'transition-all duration-300',
// Adjust padding on mobile when sidebar is hidden
'md:ml-0'
// Add left margin on desktop to account for fixed sidebar
sidebarCollapsed ? 'md:ml-16' : 'md:ml-64'
)}
>
{children}
</MainContent>
</div>
{/* CLI Stream Monitor - Global Drawer */}
<CliStreamMonitor
isOpen={isCliMonitorOpen}
onClose={handleCliMonitorClose}
/>
{/* Notification Panel - Global Drawer */}
<NotificationPanel
isOpen={isNotificationPanelVisible}
onClose={handleNotificationPanelClose}
/>
</div>
);
}

View File

@@ -15,11 +15,15 @@ import {
Settings,
User,
LogOut,
Terminal,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { useTheme } from '@/hooks';
import { LanguageSwitcher } from './LanguageSwitcher';
import { WorkspaceSelector } from '@/components/workspace/WorkspaceSelector';
import { useCliStreamStore, selectActiveExecutionCount } from '@/stores/cliStreamStore';
export interface HeaderProps {
/** Callback to toggle mobile sidebar */
@@ -30,6 +34,8 @@ export interface HeaderProps {
onRefresh?: () => void;
/** Whether refresh is in progress */
isRefreshing?: boolean;
/** Callback to open CLI monitor */
onCliMonitorClick?: () => void;
}
export function Header({
@@ -37,9 +43,11 @@ export function Header({
projectPath = '',
onRefresh,
isRefreshing = false,
onCliMonitorClick,
}: HeaderProps) {
const { formatMessage } = useIntl();
const { isDark, toggleTheme } = useTheme();
const activeCliCount = useCliStreamStore(selectActiveExecutionCount);
const handleRefresh = useCallback(() => {
if (onRefresh && !isRefreshing) {
@@ -47,11 +55,6 @@ export function Header({
}
}, [onRefresh, isRefreshing]);
// Get display path (truncate if too long)
const displayPath = projectPath.length > 40
? '...' + projectPath.slice(-37)
: projectPath || formatMessage({ id: 'navigation.header.noProject' });
return (
<header
className="flex items-center justify-between px-4 md:px-5 h-14 bg-card border-b border-border sticky top-0 z-50 shadow-sm"
@@ -83,14 +86,24 @@ export function Header({
{/* Right side - Actions */}
<div className="flex items-center gap-2">
{/* Project path indicator */}
{projectPath && (
<div className="hidden lg:flex items-center gap-2 px-3 py-1.5 bg-muted rounded-md text-sm text-muted-foreground max-w-[300px]">
<span className="truncate" title={projectPath}>
{displayPath}
</span>
</div>
)}
{/* CLI Monitor button */}
<Button
variant="ghost"
size="sm"
onClick={onCliMonitorClick}
className="gap-2"
>
<Terminal className="h-4 w-4" />
<span className="hidden sm:inline">CLI Monitor</span>
{activeCliCount > 0 && (
<Badge variant="default" className="h-5 px-1.5 text-xs">
{activeCliCount}
</Badge>
)}
</Button>
{/* Workspace selector */}
{projectPath && <WorkspaceSelector />}
{/* Refresh button */}
{onRefresh && (

View File

@@ -22,6 +22,11 @@ import {
LayoutDashboard,
Clock,
Zap,
GitFork,
Shield,
History,
Folder,
Network,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/Button';
@@ -58,7 +63,12 @@ const navItemDefinitions: Omit<NavItem, 'label'>[] = [
{ path: '/skills', icon: Sparkles },
{ path: '/commands', icon: Terminal },
{ path: '/memory', icon: Brain },
{ path: '/prompts', icon: History },
{ path: '/hooks', icon: GitFork },
{ path: '/explorer', icon: Folder },
{ path: '/graph', icon: Network },
{ path: '/settings', icon: Settings },
{ path: '/settings/rules', icon: Shield },
{ path: '/help', icon: HelpCircle },
];
@@ -103,7 +113,12 @@ export function Sidebar({
'/skills': 'main.skills',
'/commands': 'main.commands',
'/memory': 'main.memory',
'/prompts': 'main.prompts',
'/hooks': 'main.hooks',
'/explorer': 'main.explorer',
'/graph': 'main.graph',
'/settings': 'main.settings',
'/settings/rules': 'main.rules',
'/help': 'main.help',
};
return navItemDefinitions.map((item) => ({
@@ -127,12 +142,11 @@ export function Sidebar({
<aside
className={cn(
'bg-sidebar-background border-r border-border flex flex-col transition-all duration-300',
// Desktop styles
'hidden md:flex sticky top-14 h-[calc(100vh-56px)]',
// Desktop styles - fixed position for floating behavior
'hidden md:flex fixed left-0 top-14 h-[calc(100vh-56px)] z-40',
isCollapsed ? 'w-16' : 'w-64',
// Mobile styles
'md:translate-x-0',
mobileOpen && 'fixed left-0 top-14 flex translate-x-0 z-50 h-[calc(100vh-56px)] w-64 shadow-lg'
mobileOpen && 'flex z-50 w-64 shadow-lg'
)}
role="navigation"
aria-label={formatMessage({ id: 'navigation.header.brand' })}