Files
platform/OFFICERDEV_FRONTEND.md
T
2026-02-22 16:49:30 +00:00

42 KiB

Officer.dev Frontend Architecture

Executive Summary

Officer's frontend is a modern React 19 SPA (Single Page Application) built with:

  • React 19 with automatic compiler optimizations (no useCallback/useMemo needed)
  • React Router 7 for client-side navigation
  • React Query for server state management
  • Tailwind CSS 4 with custom components for styling
  • Shadcn/ui component library base
  • WebSockets for real-time terminal and AI chat
  • Modular architecture with shared component and hook libraries

This document covers the frontend architecture, component patterns, state management, and development practices.


Table of Contents

  1. Architecture Overview
  2. Technology Stack
  3. Directory Structure
  4. Routing & Navigation
  5. Component Architecture
  6. State Management
  7. API Communication
  8. Real-Time Features
  9. Styling & Theming
  10. Performance & Optimization
  11. Development Workflow

Architecture Overview

High-Level Architecture

┌──────────────────────────────────────────────────────────┐
│                    Browser Application                    │
├──────────────────────────────────────────────────────────┤
│                                                           │
│  App.tsx (Router Setup)                                 │
│    ↓                                                     │
│  BrowserRouter                                          │
│    ├─ Authentication Routes (public)                    │
│    │  └─ Landing, SignIn, SignUp, ForgotPassword       │
│    │                                                     │
│    └─ Dashboard Routes (protected)                      │
│       ├─ HomeScreen                                     │
│       ├─ SettingsPages (Profile, System, Resources)    │
│       ├─ Automation                                     │
│       ├─ SessionListPage (Chat with Pi)                │
│       ├─ PlansScreen                                   │
│       ├─ FilesScreen                                   │
│       ├─ CodeEditorScreen                              │
│       ├─ SkillsScreen                                  │
│       ├─ TasksScreen                                   │
│       ├─ ProcessesScreen                               │
│       ├─ TaskLogsScreen                                │
│       ├─ WorkspacesScreen                              │
│       ├─ ProjectListScreen                             │
│       ├─ ProjectScreen                                 │
│       └─ TerminalScreen                                │
│                                                         │
│  ↓ Shared State/Context                                │
│                                                         │
│  ├─ AuthContext (useAuth)                              │
│  ├─ GlobalState (useGlobalState)                       │
│  ├─ ServerSettings (useServerSettings)                │
│  └─ ReactQuery (useQuery, useMutation)                 │
│                                                         │
│  ↓ WebSocket Connections                               │
│                                                         │
│  ├─ Terminal WebSocket                                 │
│  │  └─ Interactive shell in xterm.js                   │
│  │                                                      │
│  └─ Pi Chat WebSocket                                  │
│     └─ Real-time streaming chat with Claude            │
│                                                         │
└──────────────────────────────────────────────────────────┘
         ↓ HTTP/REST & WebSocket
    ┌────────────────┐
    │  Officer API   │
    │  (port 5000)   │
    └────────────────┘

Data Flow

User Interaction (Click, Type, etc.)
    ↓
Component Event Handler
    ↓
State Update (useState/Context/Query)
    ↓
API Call (HTTP or WebSocket)
    ↓
Server Processing
    ↓
Response
    ↓
Update Component State
    ↓
Re-render
    ↓
Updated UI

Technology Stack

Core Frontend Framework

  • React 19 - Latest with automatic compiler optimizations
  • React DOM 19 - DOM rendering
  • React Router 7 - Client-side routing (file-based patterns support)
  • TypeScript 5.9 - Strict mode enabled

State Management

  • React Context - Component tree data sharing
  • React Query (TanStack) - Server state, caching, synchronization
  • Custom hooks - Encapsulated business logic
  • Zustand - Optional lightweight state (if used)

UI & Styling

  • Tailwind CSS 4 - Utility-first CSS framework
  • Shadcn/ui - Accessible component library base
  • Radix UI - Headless components for accessibility
  • Lucide React - Icon library
  • Tabler Icons - Additional icons
  • Class Variance Authority (CVA) - Component style variations

Real-Time Communication

  • WebSocket API - Native browser WebSocket
  • Json RPC - Message protocol over WebSocket

Code Editor & Terminal

  • Monaco Editor (@monaco-editor/react) - VS Code-like editor
  • xterm.js (@xterm/xterm) - Terminal emulator
  • @uiw/react-textarea-code-editor - Simple code editing

Data Visualization & Rich Content

  • Recharts - Chart library
  • React Markdown - Markdown rendering
  • Shiki - Syntax highlighting
  • HTML2Canvas - Screenshot capture
  • React Three Fiber - 3D rendering (if used)

Form & Validation

  • React Hook Form - Form state management
  • Zod - Schema validation
  • @hookform/resolvers - Form validation resolvers

UI Components & Utilities

  • Sonner - Toast notifications
  • Vaul - Drawer component
  • React Resizable Panels - Resizable layout panels
  • React Virtual - Virtual scrolling
  • Input OTP - OTP input component
  • React Spinners - Loading animations
  • React CountUp - Animated numbers

Authentication

  • @simplewebauthn/browser - WebAuthn/passkey support
  • @react-oauth/google - Google OAuth integration
  • JWT Decode - Token decoding

External Services

  • Googleapis - Google API integration
  • Nodemailer (backend) - Email sending

Development & Build Tools

  • Bun - Runtime and package manager
  • Vite - Build tool and dev server (if used)
  • Playwright - E2E testing
  • Testing Library - Component testing utilities
  • Happy DOM - DOM testing

Directory Structure

Frontend Layout

src/
├── apps/
│   └── officer-web/                 # Main dashboard app
│       ├── App.tsx                  # Root component & routing
│       ├── frontend.tsx             # Vite entry point (if applicable)
│       ├── index.html               # HTML template
│       │
│       ├── Screens/                 # Page-level components
│       │   ├── Authentication/      # Auth screens (public)
│       │   │   ├── LandingPage.tsx
│       │   │   ├── VerifyScreen.tsx
│       │   │   ├── SignInScreen.tsx
│       │   │   ├── SignUpScreen.tsx
│       │   │   ├── ForgotPassword.tsx
│       │   │   ├── ResetPassword.tsx
│       │   │   └── SignoutScreen.tsx
│       │   │
│       │   └── Dashboard/            # Dashboard screens (protected)
│       │       ├── HomeScreen.tsx
│       │       ├── SettingsPages/
│       │       │   ├── ProfileSettings.tsx
│       │       │   ├── SystemSettings.tsx
│       │       │   └── ResourceSettings.tsx
│       │       ├── Automation.tsx
│       │       ├── SessionListPage.tsx (Pi Chat)
│       │       ├── PlansScreen.tsx
│       │       ├── FilesScreen.tsx
│       │       ├── CodeEditorScreen.tsx
│       │       ├── SkillsScreen.tsx
│       │       ├── TasksScreen.tsx
│       │       ├── ProcessesScreen.tsx
│       │       ├── TaskLogsScreen.tsx
│       │       ├── WorkspacesScreen.tsx
│       │       ├── WorkspaceScreen.tsx
│       │       ├── ProjectListScreen.tsx
│       │       ├── ProjectScreen.tsx
│       │       └── TerminalScreen.tsx
│       │
│       ├── state/                   # Component state
│       │   ├── useAuth.ts          # Authentication state
│       │   ├── useInitialData.ts   # Initial data loading
│       │   └── useServerSettings.ts # Server configuration
│       │
│       ├── lib/                     # Client utilities
│       │   ├── api.ts              # API client
│       │   └── websocket.ts        # WebSocket utilities
│       │
│       ├── styles/                 # Global styles
│       │   ├── global.css
│       │   └── tailwind.css
│       │
│       └── locales/                # Translations
│           ├── en.json
│           └── ...
│
└── workspaces/                      # Shared libraries
    ├── components/                  # Reusable React components
    │   ├── ui/                      # Basic UI components
    │   │   ├── button.tsx
    │   │   ├── input.tsx
    │   │   ├── dialog.tsx
    │   │   ├── dropdown-menu.tsx
    │   │   ├── select.tsx
    │   │   ├── tabs.tsx
    │   │   ├── card.tsx
    │   │   └── ...more
    │   │
    │   ├── Complex/                 # Feature components
    │   │   ├── DataTable/
    │   │   ├── MarkdownEditor.tsx
    │   │   ├── ColorPicker.tsx
    │   │   ├── CommandBlock.tsx
    │   │   └── ...
    │   │
    │   ├── Workspace/               # Workspace-specific
    │   ├── Dialogs/                 # Dialog components
    │   ├── ErrorDialogs/            # Error handling UI
    │   ├── Logos/                   # Logo variants
    │   └── package.json
    │
    ├── hooks/                       # Custom React hooks
    │   ├── useAuth.ts              # Auth state hook
    │   ├── useClient.ts            # API client hook
    │   ├── useQuery.ts             # Data fetching
    │   ├── useMutation.ts          # Data mutation
    │   └── ...
    │
    ├── helpers/                     # Utility functions
    │   ├── formatters.ts           # Date, number formatting
    │   ├── validators.ts           # Input validation
    │   ├── converters.ts           # Type conversions
    │   └── ...
    │
    ├── state/                       # Global state
    │   ├── useGlobalState.ts       # Global state hook
    │   └── ...
    │
    ├── types/                       # Type definitions
    │   └── index.ts                # Export all types
    │
    ├── config/                      # Configuration
    │   ├── constants.ts
    │   └── env.ts
    │
    ├── definitions/                 # Enums and constants
    │   ├── roles.ts
    │   ├── statuses.ts
    │   └── ...
    │
    ├── widgets/                     # Complex UI widgets
    │   └── ...
    │
    └── i18n/                        # Internationalization
        └── ...

Routing & Navigation

React Router Setup: App.tsx

import { BrowserRouter, Routes, Route, Navigate } from 'react-router';
import { useAuth } from 'hooks/useAuth';
import { useServerSettings } from 'state/useServerSettings';

export function App() {
  const { isLoading, isAuthenticated } = useAuth();
  const { onboardingComplete, isLoading: isServerSettingsLoading } = useServerSettings();
  
  // Show nothing while loading auth state
  if (isLoading || isServerSettingsLoading) return null;

  return (
    <BrowserRouter>
      {!isAuthenticated && (
        <Authentication.AuthenticationLayout>
          <Routes>
            <Route path="/" element={<Authentication.LandingPage />} />
            <Route path="/auth/verify" element={<Authentication.VerifyScreen />} />
            <Route path="/auth/forgot-password" element={<Authentication.ForgotPassword />} />
            <Route path="/auth/reset-password" element={<Authentication.ResetPassword />} />
            <Route path="*" element={<Navigate to="/" replace />} />
          </Routes>
        </Authentication.AuthenticationLayout>
      )}
      
      {isAuthenticated && onboardingComplete && (
        <Dashboard.DashboardLayout>
          <Routes>
            {/* Home & Settings */}
            <Route path="/" element={<Dashboard.HomeScreen />} />
            <Route path="/settings/profile" element={<Dashboard.ProfileSettings />} />
            <Route path="/settings/system" element={<Dashboard.SystemSettings />} />
            <Route path="/settings/resources" element={<Dashboard.ResourceSettings />} />
            
            {/* Features */}
            <Route path="/automation" element={<Dashboard.Automation />} />
            <Route path="/chat/new" element={<Dashboard.SessionListPage isNew />} />
            <Route path="/chat/:sessionId" element={<Dashboard.SessionListPage />} />
            <Route path="/plans" element={<Dashboard.Plans />} />
            <Route path="/files" element={<Dashboard.FilesScreen />} />
            <Route path="/code-editor" element={<Dashboard.CodeEditor />} />
            <Route path="/skills" element={<Dashboard.Skills />} />
            <Route path="/tasks" element={<Dashboard.Tasks />} />
            <Route path="/processes" element={<Dashboard.Processes />} />
            <Route path="/task-logs" element={<Dashboard.TaskLogs />} />
            
            {/* Workspaces & Projects */}
            <Route path="/workspaces" element={<Dashboard.WorkspacesScreen />} />
            <Route path="/workspaces/:id" element={<Dashboard.WorkspaceScreen />} />
            <Route path="/projects" element={<Dashboard.ProjectListScreen />} />
            <Route path="/projects/new" element={<Dashboard.NewProjectRedirect />} />
            <Route path="/projects/:id" element={<Dashboard.ProjectScreen />} />
            
            {/* Development */}
            <Route path="/terminal" element={<Dashboard.TerminalScreen />} />
            
            {/* Auth */}
            <Route path="/auth/signout" element={<Authentication.SignoutScreen />} />
            
            {/* Fallback */}
            <Route path="*" element={<Navigate to="/" replace />} />
          </Routes>
        </Dashboard.DashboardLayout>
      )}
    </BrowserRouter>
  );
}

Layout Components

AuthenticationLayout - Wrapper for public pages

export const AuthenticationLayout = ({ children }: Props) => {
  return (
    <div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800">
      {children}
    </div>
  );
};

DashboardLayout - Wrapper for protected pages

export const DashboardLayout = ({ children }: Props) => {
  return (
    <div className="flex h-screen">
      <Sidebar />
      <main className="flex-1 flex flex-col">
        <Header />
        <div className="flex-1 overflow-auto">
          {children}
        </div>
      </main>
    </div>
  );
};

Component Architecture

Component Patterns

1. Functional Components with Props

All components are functional components using React 19:

type ButtonProps = {
  children: React.ReactNode;
  onClick: () => void;
  variant?: 'primary' | 'secondary' | 'outline';
  size?: 'sm' | 'md' | 'lg';
  disabled?: boolean;
};

export const Button = ({ 
  children, 
  onClick, 
  variant = 'primary',
  size = 'md',
  disabled = false,
}: ButtonProps) => {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      className={cn(
        'font-medium rounded-lg transition-colors',
        {
          'bg-blue-600 text-white hover:bg-blue-700': variant === 'primary',
          'bg-slate-200 text-slate-900 hover:bg-slate-300': variant === 'secondary',
          'border-2 border-blue-600 text-blue-600 hover:bg-blue-50': variant === 'outline',
        },
        {
          'px-3 py-1 text-sm': size === 'sm',
          'px-4 py-2 text-base': size === 'md',
          'px-6 py-3 text-lg': size === 'lg',
        },
        disabled && 'opacity-50 cursor-not-allowed',
      )}
    >
      {children}
    </button>
  );
};

2. Component Organization

Complex components follow a structured pattern:

// Directory structure
ComponentName/
├── index.tsx          # Exports the component
├── ComponentName.tsx  # Main implementation
├── hooks/             # Local hooks
   └── useComponentState.ts
├── types.ts           # Component types
└── utils.ts           # Helper functions

// index.tsx
export { ComponentName } from './ComponentName';
export type { ComponentNameProps } from './types';

// ComponentName.tsx
import { useComponentState } from './hooks/useComponentState';
import type { ComponentNameProps } from './types';

export const ComponentName = ({ prop1, prop2 }: ComponentNameProps) => {
  const { state, actions } = useComponentState();
  
  return (
    <div>
      {/* Render */}
    </div>
  );
};

3. React 19 Patterns

NO useCallback - React 19's compiler handles optimization:

// ❌ BAD - Unnecessary useCallback
export const Form = () => {
  const handleSubmit = useCallback((data) => {
    api.post('/data', data);
  }, []);
  
  return <FormInput onSubmit={handleSubmit} />;
};

// ✅ GOOD - Plain function
export const Form = () => {
  const handleSubmit = (data) => {
    api.post('/data', data);
  };
  
  return <FormInput onSubmit={handleSubmit} />;
};

NO useMemo - Compiler handles memoization:

// ❌ BAD - Unnecessary useMemo
export const List = ({ items, filter }) => {
  const filtered = useMemo(
    () => items.filter(i => i.type === filter),
    [items, filter]
  );
  
  return <ul>{filtered.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
};

// ✅ GOOD - Direct calculation
export const List = ({ items, filter }) => {
  const filtered = items.filter(i => i.type === filter);
  
  return <ul>{filtered.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
};

Avoid stale closures - access object properties directly:

// ❌ BAD - Destructuring creates stale references
export const Editor = ({ state }) => {
  const { content, save } = state;
  
  useEffect(() => {
    const timer = setTimeout(() => {
      save(content); // 'content' is stale!
    }, 1000);
    return () => clearTimeout(timer);
  }, [content, save]);
};

// ✅ GOOD - Direct property access
export const Editor = ({ state }) => {
  useEffect(() => {
    const timer = setTimeout(() => {
      state.save(state.content); // Always fresh
    }, 1000);
    return () => clearTimeout(timer);
  }, [state]);
};

4. Keyboard Event Handling

Always prevent default for game/editor controls:

export const CodeEditor = () => {
  const handleKeyDown = (ev: KeyboardEvent) => {
    // Prevent Space from scrolling page
    if (ev.code === 'Space') {
      ev.preventDefault();
      // Handle space key
    }
    
    // Prevent Escape from closing dialogs
    if (ev.key === 'Escape') {
      ev.preventDefault();
      // Handle escape
    }
  };
  
  return <div onKeyDown={handleKeyDown}>{/* ... */}</div>;
};

5. Button Focus Management

Buttons retain focus after clicking, interfering with keyboard shortcuts:

export const Toolbar = () => {
  return (
    <>
      <button
        onClick={(e) => {
          handleSave();
          e.currentTarget.blur(); // Remove focus to prevent Space re-triggering
        }}
      >
        Save
      </button>
    </>
  );
};

State Management

Authentication State: useAuth

// Location: workspaces/hooks/useAuth.ts

export const useAuth = () => {
  const [isLoading, setIsLoading] = useState(true);
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [user, setUser] = useState<User | null>(null);
  const [token, setToken] = useState<string | null>(
    () => localStorage.getItem('token')
  );

  // Load auth state on mount
  useEffect(() => {
    const load = async () => {
      if (!token) {
        setIsAuthenticated(false);
        setIsLoading(false);
        return;
      }

      try {
        // Verify token validity
        const response = await fetch('/api/auth/verify', {
          headers: { Authorization: `Bearer ${token}` },
        });

        if (response.ok) {
          const user = await response.json();
          setUser(user);
          setIsAuthenticated(true);
        } else {
          setIsAuthenticated(false);
          localStorage.removeItem('token');
        }
      } catch {
        setIsAuthenticated(false);
      } finally {
        setIsLoading(false);
      }
    };

    load();
  }, [token]);

  const login = async (email: string, password: string) => {
    const response = await fetch('/api/auth/signin', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    });

    if (response.ok) {
      const { token, user } = await response.json();
      localStorage.setItem('token', token);
      setToken(token);
      setUser(user);
      setIsAuthenticated(true);
      return { success: true };
    }

    return { success: false, error: await response.text() };
  };

  const logout = async () => {
    try {
      await fetch('/api/auth/signout', {
        method: 'POST',
        headers: { Authorization: `Bearer ${token}` },
      });
    } catch {
      // Ignore errors during logout
    } finally {
      localStorage.removeItem('token');
      setToken(null);
      setUser(null);
      setIsAuthenticated(false);
    }
  };

  return {
    isLoading,
    isAuthenticated,
    user,
    token,
    login,
    logout,
  };
};

// Usage in components
export const Dashboard = () => {
  const { user, logout } = useAuth();
  
  return (
    <div>
      <p>Welcome, {user?.email}</p>
      <button onClick={logout}>Logout</button>
    </div>
  );
};

Server Settings State: useServerSettings

export const useServerSettings = () => {
  const [isLoading, setIsLoading] = useState(true);
  const [onboardingComplete, setOnboardingComplete] = useState(false);
  const [plugins, setPlugins] = useState<Plugin[]>([]);
  const [settings, setSettings] = useState<ServerSettings | null>(null);

  useEffect(() => {
    const load = async () => {
      try {
        const response = await fetch('/api/server-settings');
        if (response.ok) {
          const data = await response.json();
          setOnboardingComplete(data.onboardingComplete);
          setPlugins(data.plugins);
          setSettings(data.settings);
        }
      } catch {
        console.error('Failed to load server settings');
      } finally {
        setIsLoading(false);
      }
    };

    load();
  }, []);

  return {
    isLoading,
    onboardingComplete,
    plugins,
    settings,
  };
};

Global State: useGlobalState

For component-tree-wide state, use Context:

type GlobalContextType = {
  theme: 'light' | 'dark';
  setTheme: (theme: 'light' | 'dark') => void;
  sidebarOpen: boolean;
  setSidebarOpen: (open: boolean) => void;
};

const GlobalContext = createContext<GlobalContextType | null>(null);

export const GlobalProvider = ({ children }: { children: React.ReactNode }) => {
  const [theme, setTheme] = useState<'light' | 'dark'>(() => {
    const saved = localStorage.getItem('theme');
    return (saved as 'light' | 'dark') || 'dark';
  });

  const [sidebarOpen, setSidebarOpen] = useState(true);

  useEffect(() => {
    localStorage.setItem('theme', theme);
    document.documentElement.classList.toggle('dark', theme === 'dark');
  }, [theme]);

  return (
    <GlobalContext.Provider value={{ theme, setTheme, sidebarOpen, setSidebarOpen }}>
      {children}
    </GlobalContext.Provider>
  );
};

export const useGlobalState = () => {
  const context = useContext(GlobalContext);
  if (!context) {
    throw new Error('useGlobalState must be used within GlobalProvider');
  }
  return context;
};

React Query for Server State

// useQueryPlan.ts
export const useQueryPlan = (id: number) => {
  return useQuery({
    queryKey: ['plans', id],
    queryFn: async () => {
      const response = await fetch(`/api/plans/${id}`, {
        headers: { Authorization: `Bearer ${localStorage.getItem('token')}` },
      });
      if (!response.ok) throw new Error('Failed to load plan');
      return response.json();
    },
  });
};

// useMutationCreatePlan.ts
export const useMutationCreatePlan = () => {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: async (data: CreatePlanInput) => {
      const response = await fetch('/api/plans', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${localStorage.getItem('token')}`,
        },
        body: JSON.stringify(data),
      });
      if (!response.ok) throw new Error('Failed to create plan');
      return response.json();
    },
    onSuccess: () => {
      // Invalidate related queries
      queryClient.invalidateQueries({ queryKey: ['plans'] });
    },
  });
};

// In component
export const CreatePlanForm = () => {
  const { mutate, isPending } = useMutationCreatePlan();
  
  const handleSubmit = (data: CreatePlanInput) => {
    mutate(data);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      {/* Form fields */}
      <button type="submit" disabled={isPending}>
        {isPending ? 'Creating...' : 'Create'}
      </button>
    </form>
  );
};

API Communication

Client Initialization: src/apps/officer-web/lib/api.ts

// Centralized API client
const API_BASE = import.meta.env.VITE_API_URL || '/api';

export const apiClient = {
  async fetch(
    endpoint: string,
    options: RequestInit = {},
  ): Promise<Response> {
    const token = localStorage.getItem('token');
    
    return fetch(`${API_BASE}${endpoint}`, {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        ...(token && { Authorization: `Bearer ${token}` }),
        ...options.headers,
      },
    });
  },

  async get<T>(endpoint: string): Promise<T> {
    const response = await this.fetch(endpoint);
    if (!response.ok) throw new Error(`GET ${endpoint} failed`);
    return response.json();
  },

  async post<T>(endpoint: string, body?: any): Promise<T> {
    const response = await this.fetch(endpoint, {
      method: 'POST',
      body: body ? JSON.stringify(body) : undefined,
    });
    if (!response.ok) throw new Error(`POST ${endpoint} failed`);
    return response.json();
  },

  async put<T>(endpoint: string, body?: any): Promise<T> {
    const response = await this.fetch(endpoint, {
      method: 'PUT',
      body: body ? JSON.stringify(body) : undefined,
    });
    if (!response.ok) throw new Error(`PUT ${endpoint} failed`);
    return response.json();
  },

  async delete<T>(endpoint: string): Promise<T> {
    const response = await this.fetch(endpoint, { method: 'DELETE' });
    if (!response.ok) throw new Error(`DELETE ${endpoint} failed`);
    return response.json();
  },
};

// Usage
const user = await apiClient.get<User>('/users/me');
await apiClient.post('/plans', { title: 'New Plan' });

Real-Time Features

Terminal WebSocket: TerminalScreen.tsx

import { XTerm } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import '@xterm/xterm/css/xterm.css';

export const TerminalScreen = () => {
  const terminalRef = useRef<HTMLDivElement>(null);
  const xtermRef = useRef<XTerm | null>(null);
  const wsRef = useRef<WebSocket | null>(null);

  useEffect(() => {
    const term = new XTerm({
      cols: 120,
      rows: 40,
      theme: { background: '#0f172a', foreground: '#e2e8f0' },
    });

    const fitAddon = new FitAddon();
    term.loadAddon(fitAddon);

    // Mount terminal
    if (terminalRef.current) {
      term.open(terminalRef.current);
      fitAddon.fit();
    }

    xtermRef.current = term;

    // Connect WebSocket
    const token = localStorage.getItem('token');
    const cwd = '/home/user'; // Or from settings
    
    const ws = new WebSocket(
      `ws://localhost:5000/api/terminal/ws?token=${token}&cwd=${encodeURIComponent(cwd)}&cols=120&rows=40`
    );

    ws.onopen = () => {
      console.log('Terminal connected');
    };

    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      
      if (message.type === 'output') {
        term.write(message.data); // Display output
      } else if (message.type === 'error') {
        console.error('Terminal error:', message.error);
      }
    };

    ws.onerror = () => {
      term.write('\r\nConnection error\r\n');
    };

    ws.onclose = () => {
      term.write('\r\nDisconnected\r\n');
    };

    // Send input from terminal
    term.onData((data) => {
      ws.send(JSON.stringify({ type: 'input', data }));
    });

    wsRef.current = ws;

    // Handle resize
    const handleResize = () => {
      fitAddon.fit();
      const { cols, rows } = term;
      ws.send(JSON.stringify({ type: 'resize', cols, rows }));
    };

    window.addEventListener('resize', handleResize);

    // Cleanup
    return () => {
      window.removeEventListener('resize', handleResize);
      ws.close();
      term.dispose();
    };
  }, []);

  return <div ref={terminalRef} className="w-full h-full" />;
};

Pi Chat WebSocket: SessionListPage.tsx

export const SessionListPage = ({ sessionId, isNew }: Props) => {
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const [input, setInput] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const wsRef = useRef<WebSocket | null>(null);

  // Connect to Pi chat WebSocket
  useEffect(() => {
    const token = localStorage.getItem('token');
    const url = `ws://localhost:5000/api/pi/chat/ws?token=${token}&sessionId=${sessionId}`;

    const ws = new WebSocket(url);

    ws.onopen = () => {
      console.log('Pi chat connected');
    };

    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      
      switch (message.type) {
        case 'message_update':
          // Stream response text
          if (message.event.type === 'text_delta') {
            setMessages(prev => {
              const last = prev[prev.length - 1];
              if (last && last.role === 'assistant') {
                return [
                  ...prev.slice(0, -1),
                  { ...last, content: last.content + message.event.delta },
                ];
              }
              return prev;
            });
          }
          break;
          
        case 'tool_execution_start':
          setMessages(prev => [
            ...prev,
            { role: 'tool', toolName: message.toolName, content: 'Executing...' },
          ]);
          break;
          
        case 'agent_end':
          setIsLoading(false);
          break;
      }
    };

    ws.onerror = (error) => {
      console.error('Pi chat error:', error);
      setIsLoading(false);
    };

    wsRef.current = ws;

    return () => ws.close();
  }, [sessionId]);

  // Send message
  const handleSendMessage = () => {
    if (!input.trim() || !wsRef.current) return;

    const userMessage = input;
    setInput('');
    setIsLoading(true);

    // Add user message to UI
    setMessages(prev => [
      ...prev,
      { role: 'user', content: userMessage },
    ]);

    // Send to Pi
    wsRef.current.send(JSON.stringify({
      type: 'prompt',
      text: userMessage,
    }));
  };

  return (
    <div className="flex flex-col h-full">
      <div className="flex-1 overflow-auto p-4">
        {messages.map((msg, i) => (
          <div key={i} className={cn('mb-4', msg.role === 'user' ? 'text-right' : 'text-left')}>
            <div className={cn('inline-block p-2 rounded', 
              msg.role === 'user' ? 'bg-blue-600 text-white' : 'bg-slate-200'
            )}>
              {msg.content}
            </div>
          </div>
        ))}
        {isLoading && <div>Loading...</div>}
      </div>

      <div className="p-4 border-t">
        <div className="flex gap-2">
          <input
            value={input}
            onChange={(e) => setInput(e.target.value)}
            onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
            placeholder="Ask Pi anything..."
            disabled={isLoading}
            className="flex-1 px-4 py-2 border rounded"
          />
          <button
            onClick={handleSendMessage}
            disabled={isLoading || !input.trim()}
            className="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
          >
            Send
          </button>
        </div>
      </div>
    </div>
  );
};

Styling & Theming

Tailwind CSS Integration

Officer uses Tailwind CSS 4 with a custom configuration:

// tailwind.config.js
export default {
  content: [
    './src/**/*.{ts,tsx}',
  ],
  theme: {
    extend: {
      colors: {
        // Custom color palette if needed
      },
      spacing: {
        // Custom spacing
      },
      animation: {
        // Custom animations
      },
    },
  },
  plugins: [
    require('tailwindcss-animate'),
  ],
};

Shadcn/ui Components

Reusable UI components from shadcn/ui:

// Button component wrapper
import { Button as ShadcnButton } from '@/components/ui/button';

export const Button = (props) => (
  <ShadcnButton {...props} />
);

// Usage
<Button variant="outline" size="lg">
  Click me
</Button>

Custom Theming

Support for light/dark themes:

// Global theme management
export const useTheme = () => {
  const { theme, setTheme } = useGlobalState();
  
  useEffect(() => {
    document.documentElement.classList.toggle('dark', theme === 'dark');
    localStorage.setItem('theme', theme);
  }, [theme]);
  
  return { theme, setTheme };
};

// In component
export const ThemeToggle = () => {
  const { theme, setTheme } = useTheme();
  
  return (
    <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
      {theme === 'dark' ? '☀️' : '🌙'}
    </button>
  );
};

Performance & Optimization

Image Optimization

Use Next Image component or lazy loading:

import { lazy, Suspense } from 'react';

// Lazy load heavy components
const CodeEditor = lazy(() => import('./CodeEditor'));

export const FeaturePage = () => {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <CodeEditor />
    </Suspense>
  );
};

Virtual Scrolling for Large Lists

Use React Virtual for efficient rendering:

import { useVirtualizer } from '@tanstack/react-virtual';

export const VirtualList = ({ items }: { items: Item[] }) => {
  const parentRef = useRef<HTMLDivElement>(null);
  
  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
  });

  return (
    <div ref={parentRef} className="h-96 overflow-auto">
      <div style={{ height: `${virtualizer.getTotalSize()}px` }}>
        {virtualizer.getVirtualItems().map(virtualItem => (
          <div key={virtualItem.key} data-index={virtualItem.index}>
            {items[virtualItem.index]?.name}
          </div>
        ))}
      </div>
    </div>
  );
};

Code Splitting with React Router

Routes are automatically code-split:

// Lazy load screen components
const HomeScreen = lazy(() => import('./HomeScreen'));
const SettingsScreen = lazy(() => import('./SettingsScreen'));

<Suspense fallback={<LoadingSpinner />}>
  <Routes>
    <Route path="/" element={<HomeScreen />} />
    <Route path="/settings" element={<SettingsScreen />} />
  </Routes>
</Suspense>

React Query Caching

Automatic caching and background updates:

// Data is cached per queryKey
const { data: plans } = useQuery({
  queryKey: ['plans'], // Cached
  queryFn: fetchPlans,
  staleTime: 1000 * 60 * 5, // 5 minutes
  gcTime: 1000 * 60 * 10, // 10 minutes (was cacheTime)
});

// Background refetch when window regains focus
useQuery({
  queryKey: ['plans'],
  queryFn: fetchPlans,
  refetchOnWindowFocus: true,
});

Development Workflow

Development Server

# Start Bun dev server with hot reload
bun dev

# Runs on http://localhost:5000/
# Frontend and backend both reload on file changes

Environment Variables

# .env (client-side, public)
VITE_API_URL=http://localhost:5000/api

TypeScript Type Checking

# Run TypeScript compiler
tsc --noEmit

# Included in build process
bun run build

Code Formatting

# Format all files
bun format

# Format specific files
bun format:check

# Prettier config in .prettierrc
{
  "semi": true,
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 120
}

Component Development

When creating new features:

  1. Create component structure
src/apps/officer-web/Screens/MyFeature/
├── index.tsx          # Exports
├── MyFeatureScreen.tsx # Component
├── hooks/
│   └── useMyFeature.ts
├── types.ts
└── utils.ts
  1. Define types
// types.ts
export type MyFeatureProps = {
  onSubmit: (data: FormData) => void;
  disabled?: boolean;
};
  1. Implement component
// MyFeatureScreen.tsx
import { useMyFeature } from './hooks/useMyFeature';
import type { MyFeatureProps } from './types';

export const MyFeatureScreen = ({ onSubmit, disabled }: MyFeatureProps) => {
  const { state, actions } = useMyFeature();
  
  return (/* JSX */);
};
  1. Export
// index.tsx
export { MyFeatureScreen } from './MyFeatureScreen';
export type { MyFeatureProps } from './types';

Integration Points

API Endpoints Used

  • Auth: /api/auth/* (signin, signup, verify, etc.)
  • Plans: /api/plans (GET, POST, PUT, DELETE)
  • Skills: /api/skills (GET, POST)
  • Tasks: /api/tasks (CRUD operations)
  • Files: /api/file-browser (navigation, upload)
  • Settings: /api/user (preferences)
  • Terminal WS: /api/terminal/ws (real-time shell)
  • Pi Chat WS: /api/pi/chat/ws (real-time AI assistant)

State Flow from Server to UI

Database
  ↓
API Response
  ↓
React Query Cache
  ↓
Component State
  ↓
Rendered UI

Debugging

Browser DevTools

// Log component props
console.log('Props:', props);

// Log state updates
console.log('State changed:', newState);

// React DevTools browser extension
// Inspect component tree and props

React Query Devtools

// Installed: @tanstack/react-query-devtools

import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

export function App() {
  return (
    <>
      {/* App content */}
      <ReactQueryDevtools initialIsOpen={false} />
    </>
  );
}

Common Patterns

Loading States

export const DataComponent = () => {
  const { data, isLoading, error } = useQuery({
    queryKey: ['data'],
    queryFn: fetchData,
  });

  if (isLoading) return <LoadingSpinner />;
  if (error) return <ErrorMessage error={error} />;
  if (!data) return <EmptyState />;

  return <DataView data={data} />;
};

Form Handling with React Hook Form

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const schema = z.object({
  email: z.string().email('Invalid email'),
  password: z.string().min(8, 'Min 8 chars'),
});

type FormData = z.infer<typeof schema>;

export const LoginForm = () => {
  const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
    resolver: zodResolver(schema),
  });

  const onSubmit = async (data: FormData) => {
    await apiClient.post('/auth/signin', data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email')} placeholder="Email" />
      {errors.email && <span>{errors.email.message}</span>}
      
      <input {...register('password')} type="password" />
      {errors.password && <span>{errors.password.message}</span>}
      
      <button type="submit">Login</button>
    </form>
  );
};

Toast Notifications

import { toast } from 'sonner';

// Success
toast.success('Operation completed!');

// Error
toast.error('Something went wrong', {
  description: 'Please try again later',
});

// Custom
toast.custom((t) => (
  <div>Custom notification</div>
));

  • Backend Architecture: See OFFICERDEV_BACKEND.md
  • CONVENTIONS.md: Detailed code patterns and style
  • CLAUDE.md: Project overview
  • apps/officer-web/CLAUDE.md: Frontend-specific patterns

Summary

Officer's frontend is a modern React 19 SPA featuring:

  • Type-safe component architecture
  • Real-time WebSocket communication
  • Server state management with React Query
  • Beautiful UI with Tailwind CSS and shadcn/ui
  • Strong React patterns without useCallback/useMemo
  • Comprehensive authentication and authorization
  • Responsive design for all devices
  • AI-powered development assistance via Pi
  • Interactive terminal emulation

The architecture prioritizes developer experience, type safety, and performance while providing a rich, interactive user experience for life management and AI-assisted development.