mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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' })}
|
||||
|
||||
Reference in New Issue
Block a user