Skip to content

[Feature] improve request #20

@benjamin920101

Description

@benjamin920101

Executive Summary

This report identifies 14 major optimization categories with 60+ specific actionable items across the OpenRoom codebase. Issues range from critical security concerns to performance improvements, code quality enhancements, and developer experience optimizations.


Table of Contents

  1. Build & Bundle Optimization
  2. TypeScript Configuration
  3. Code Quality & Type Safety
  4. React Performance
  5. State Management
  6. Testing Coverage
  7. Dependency Optimization
  8. Security Concerns
  9. i18n Implementation
  10. File System & Storage
  11. LLM Client Optimization
  12. Monorepo Structure
  13. Accessibility (a11y)
  14. Documentation & DX
  15. Priority Summary

1. Build & Bundle Optimization

Current State

  • Vite Version: 4.4.5 (outdated, latest is 6.x)
  • Legacy Plugin: @vitejs/plugin-legacy adds IE11 polyfill bloat
  • No Code Splitting: All apps bundle together
  • Chunk Warning Limit: 1500KB with no optimization strategy

Issues

// package.json - Outdated dependencies
"@vitejs/plugin-legacy": "5.4.1",  // Adds unnecessary polyfills
"vite": "^4.4.5"  // Behind latest (6.x)

Recommendations

1.1 Upgrade Vite and Remove Legacy Plugin

pnpm update vite @vitejs/plugin-react-swc
# Consider removing @vitejs/plugin-legacy if IE11 support not required

1.2 Implement Manual Code Splitting

// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vendor-react': ['react', 'react-dom', 'react-router-dom'],
          'vendor-three': ['three', '@react-three/fiber', '@react-three/drei'],
          'vendor-animation': ['framer-motion', '@react-spring/three'],
          'vendor-editor': ['@tiptap/react', '@tiptap/starter-kit', 'tiptap-markdown'],
          'vendor-i18n': ['i18next', 'react-i18next'],
          'vendor-markdown': ['react-markdown', 'remark-gfm', 'rehype-raw'],
        },
      },
    },
  },
});

1.3 Enable Tree Shaking for Icons

// Instead of importing all icons, use tree-shakeable imports
// ❌ Before
import { Play, Pause, SkipBack } from 'lucide-react';

// ✅ After (already done, but verify across all files)
import { Play } from 'lucide-react';

1.4 Add Bundle Analysis Script

// package.json
{
  "scripts": {
    "build:analyze": "ANALYZE=true pnpm build"
  }
}
// vite.config.ts - Already configured, ensure it's used
import { visualizer } from 'rollup-plugin-visualizer';

if (process.env.ANALYZE) {
  plugins.push(
    visualizer({
      gzipSize: true,
      open: true,
      filename: 'dist/stats.html',
    }),
  );
}

2. TypeScript Configuration

Current State

// tsconfig.json
{
  "compilerOptions": {
    "moduleResolution": "node",  // Outdated resolution strategy
    "noUnusedLocals": true,      // Enabled but violations exist
    "noUnusedParameters": true,  // Enabled but violations exist
  }
}

Issues

Setting Current Recommended Reason
moduleResolution "node" "bundler" Vite best practice, better ESM support
verbatimModuleSyntax Missing true Prevents ESM/CJS interop issues
noUnusedLocals true false Conflicts with actual usage patterns
noUnusedParameters true false Often needed for callback signatures

Recommendations

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "verbatimModuleSyntax": true,
    "strict": true,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "noFallthroughCasesInSwitch": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    },
    "types": ["vitest/globals"]
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx"],
  "exclude": ["node_modules"]
}

3. Code Quality & Type Safety

Current State

  • 3 occurrences of as any in action.ts
  • 157 console.log statements across codebase
  • Lenient ESLint rules

Issues

3.1 Type Unsafe Code

// apps/webuiapps/src/lib/action.ts - Lines 118, 197, 211
manager.sendAgentMessage(event as any);  // ❌ Bypasses type checking

3.2 Inconsistent Logging

// Scattered throughout codebase
console.log('[LLM] Request:', data);     // ❌ Should use logger
console.warn('[diskStorage] failed:', e); // ❌ Should use logger
console.error('[AgentAction] Error:', err); // ❌ Should use logger

// Correct usage (already exists in project)
import { logger } from './logger';
logger.info('LLM', 'Request:', data);    // ✅ Consistent, configurable

3.3 ESLint Rule Gaps

// .eslintrc - Current rules
{
  "rules": {
    "@typescript-eslint/no-explicit-any": "warn",  // Should be error
    "no-console": ["warn", { "allow": ["warn", "error"] }], // Too lenient
    // Missing:
    // - react-hooks/exhaustive-deps
    // - @typescript-eslint/no-unused-vars (configured but violations exist)
  }
}

Recommendations

3.4 Fix Type Safety

// apps/webuiapps/src/lib/action.ts

// Option 1: Fix the type definition
interface AgentMessagePayload {
  content: string;
  // Add other expected properties
}

// Then use proper typing
manager.sendAgentMessage({ content: JSON.stringify(event) });

// Option 2: Use type assertion with explanation
// eslint-disable-next-line @typescript-eslint/no-explicit-any
manager.sendAgentMessage(event as unknown as AgentMessagePayload);

3.5 Update ESLint Configuration

// .eslintrc
{
  "env": {
    "es2021": true,
    "browser": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-requiring-type-checking",
    "plugin:react/recommended",
    "plugin:react-hooks/recommended",
    "plugin:prettier/recommended"
  ],
  "parser": "@typescript-eslint/parser",
  "plugins": [
    "@typescript-eslint",
    "eslint-plugin-import",
    "eslint-plugin-react",
    "eslint-plugin-react-hooks"
  ],
  "parserOptions": {
    "ecmaVersion": 12,
    "sourceType": "module",
    "project": "./tsconfig.json"
  },
  "rules": {
    "eqeqeq": "error",
    "no-var": "error",
    "no-alert": "error",
    "no-console": ["error", { "allow": ["warn", "error"] }],
    "no-use-before-define": "warn",
    "no-unused-vars": "off",
    "@typescript-eslint/no-unused-vars": ["error", { 
      "argsIgnorePattern": "^_",
      "varsIgnorePattern": "^_"
    }],
    "@typescript-eslint/no-explicit-any": "error",
    "@typescript-eslint/explicit-function-return-type": "off",
    "@typescript-eslint/no-non-null-assertion": "warn",
    "@typescript-eslint/ban-ts-comment": ["error", {
      "ts-ignore": "allow-with-description",
      "ts-expect-error": "allow-with-description"
    }],
    "@typescript-eslint/no-unsafe-assignment": "warn",
    "@typescript-eslint/no-unsafe-member-access": "warn",
    "@typescript-eslint/no-unsafe-call": "warn",
    "react-hooks/exhaustive-deps": "error",
    "react/prop-types": "off",
    "import/no-anonymous-default-export": [
      "warn",
      {
        "allowArray": false,
        "allowArrowFunction": true,
        "allowAnonymousClass": false,
        "allowAnonymousFunction": false,
        "allowCallExpression": true,
        "allowLiteral": false,
        "allowObject": false
      }
    ],
    "no-throw-literal": "error",
    "no-unmodified-loop-condition": "error"
  },
  "settings": {
    "react": {
      "version": "detect"
    },
    "import/resolver": {
      "typescript": true,
      "node": true
    }
  }
}

3.6 Replace console.log with Logger

Create a script to identify and fix:

# Find all console.log statements
grep -rn "console\.\(log\|warn\|error\|info\|debug\)" apps/webuiapps/src --include="*.ts" --include="*.tsx"

Then replace systematically:

// Before
console.log('[LLM] Sending request:', config);

// After
logger.info('LLM', 'Sending request:', config);

4. React Performance

Current State

  • ChatPanel.tsx: 1467 lines (single component doing too much)
  • Limited React.memo usage: Only CharacterAvatar uses memo
  • Missing useMemo/useCallback: Expensive computations on every render
  • Potential stale closures: useEffect with missing dependencies

Issues

4.1 Monolithic Component

// apps/webuiapps/src/components/ChatPanel/index.tsx
// 1467 lines handling:
// - LLM chat logic
// - Character system
// - Mod system  
// - Tool execution
// - Image generation
// - Chat history
// - Emotion rendering

4.2 Missing Memoization

// Tool definitions recreated on every render
const toolDefinitions = [
  getAppActionToolDefinition(),
  getListAppsToolDefinition(),
  ...getFileToolDefinitions(),
  ...getMemoryToolDefinitions(),
  ...getImageGenToolDefinitions(),
];

4.3 Inefficient Event Handlers

// New function created on every render
const handleSendMessage = async (message: string) => {
  // ... logic
};

Recommendations

4.4 Split ChatPanel Component

// Proposed structure:
// components/ChatPanel/
// ├── index.tsx          (Main container, state orchestration)
// ├── ChatPanel.tsx      (Chat UI only)
// ├── CharacterPanel.tsx (Already extracted)
// ├── ModPanel.tsx       (Already extracted)
// ├── hooks/
// │   ├── useLLMChat.ts      (LLM communication logic)
// │   ├── useToolExecution.ts (Tool handling logic)
// │   ├── useChatHistory.ts   (History persistence)
// │   └── useCharacterSystem.ts (Character state)
// └── components/
//     ├── MessageList.tsx
//     ├── MessageInput.tsx
//     ├── ToolCallDisplay.tsx
//     └── EmotionAvatar.tsx

4.5 Add Memoization

// apps/webuiapps/src/components/ChatPanel/index.tsx

// Memoize tool definitions
const toolDefinitions = useMemo(() => {
  return [
    getAppActionToolDefinition(),
    getListAppsToolDefinition(),
    ...getFileToolDefinitions(),
    ...getMemoryToolDefinitions(),
    ...getImageGenToolDefinitions(),
    getRespondToUserToolDef(),
    getFinishTargetToolDef(),
  ];
}, []);

// Memoize system prompt
const systemPrompt = useMemo(() => {
  return buildSystemPrompt(character, modManager, memories, hasImageGen);
}, [character, modManager, memories, hasImageGen]);

// Memoize handlers with useCallback
const handleSendMessage = useCallback(async (message: string) => {
  // ... logic
}, [messages, character, toolDefinitions]); // Dependencies

4.6 Add React.memo to Heavy Components

// Wrap frequently re-rendering components
export const MessageList = memo(({ messages, isLoading }: MessageListProps) => {
  return (
    <div className={styles.messageList}>
      {messages.map((msg) => (
        <Message key={msg.id} {...msg} />
      ))}
      {isLoading && <LoadingIndicator />}
    </div>
  );
});

MessageList.displayName = 'MessageList';

4.7 Fix useEffect Dependencies

// Run eslint-plugin-react-hooks to find missing dependencies
// Then fix:

// ❌ Before - missing dependencies
useEffect(() => {
  loadChatHistory().then(setMessages);
}, []);

// ✅ After - proper dependencies
useEffect(() => {
  const controller = new AbortController();
  loadChatHistory().then((history) => {
    if (!controller.signal.aborted) {
      setMessages(history);
    }
  });
  return () => controller.abort();
}, [sessionPath]); // Dependency

5. State Management

Current State

  • Mixed patterns: Context + Reducer, localStorage, IndexedDB, file system
  • No devtools: Hard to debug state changes
  • Mutable module state: windowManager.ts uses module-level variables

Issues

5.1 Mutable State in windowManager

// apps/webuiapps/src/lib/windowManager.ts
let windows: WindowState[] = [];  // ❌ Mutable module state
let nextZ = 100;
let offsetCounter = 0;

export function openWindow(appId: number): void {
  windows = [...windows, win];  // Direct mutation pattern
  notify();
}

5.2 No State Devtools

  • Cannot time-travel debug
  • Hard to track state changes
  • No state persistence for debugging

Recommendations

5.3 Migrate to Zustand

pnpm add zustand
// apps/webuiapps/src/store/windowStore.ts
import { create } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';

export interface WindowState {
  appId: number;
  title: string;
  x: number;
  y: number;
  width: number;
  height: number;
  zIndex: number;
  minimized: boolean;
}

interface WindowStore {
  windows: WindowState[];
  focusedWindowId: number | null;
  openWindow: (appId: number) => void;
  closeWindow: (appId: number) => void;
  focusWindow: (appId: number) => void;
  minimizeWindow: (appId: number) => void;
  moveWindow: (appId: number, x: number, y: number) => void;
  resizeWindow: (appId: number, width: number, height: number) => void;
}

export const useWindowStore = create<WindowStore>()(
  subscribeWithSelector((set, get) => ({
    windows: [],
    focusedWindowId: null,
    
    openWindow: (appId: number) => {
      const existing = get().windows.find((w) => w.appId === appId);
      if (existing) {
        set((state) => ({
          windows: state.windows.map((w) =>
            w.appId === appId ? { ...w, zIndex: claimZIndex(), minimized: false } : w
          ),
          focusedWindowId: appId,
        }));
        return;
      }

      const size = getAppDefaultSize(appId);
      const offset = (get().windows.length % 5) * 30;

      set((state) => ({
        windows: [
          ...state.windows,
          {
            appId,
            title: getAppDisplayName(appId),
            x: 80 + offset,
            y: 40 + offset,
            width: size.width,
            height: size.height,
            zIndex: claimZIndex(),
            minimized: false,
          },
        ],
        focusedWindowId: appId,
      }));
    },

    closeWindow: (appId: number) => {
      set((state) => ({
        windows: state.windows.filter((w) => w.appId !== appId),
        focusedWindowId: state.focusedWindowId === appId ? null : state.focusedWindowId,
      }));
    },

    focusWindow: (appId: number) => {
      set((state) => ({
        windows: state.windows.map((w) =>
          w.appId === appId ? { ...w, zIndex: claimZIndex() } : w
        ),
        focusedWindowId: appId,
      }));
    },

    minimizeWindow: (appId: number) => {
      set((state) => ({
        windows: state.windows.map((w) =>
          w.appId === appId ? { ...w, minimized: true } : w
        ),
      }));
    },

    moveWindow: (appId: number, x: number, y: number) => {
      set((state) => ({
        windows: state.windows.map((w) =>
          w.appId === appId ? { ...w, x, y } : w
        ),
      }));
    },

    resizeWindow: (appId: number, width: number, height: number) => {
      set((state) => ({
        windows: state.windows.map((w) =>
          w.appId === appId ? { ...w, width: Math.max(300, width), height: Math.max(200, height) } : w
        ),
      }));
    },
  }))
);

5.4 Add Redux Devtools (Optional)

pnpm add @redux-devtools/extension
// With redux-devtools integration
import { devtools } from 'zustand/middleware';

export const useWindowStore = create<WindowStore>()(
  devtools(subscribeWithSelector((set) => ({
    // ... state
  })), { name: 'WindowStore' })
);

6. Testing Coverage

Current State

  • 7 test files for entire codebase
  • Only lib utilities tested - No component tests
  • E2E: Chromium only
  • No coverage enforcement in CI

Issues

6.1 Limited Unit Tests

apps/webuiapps/src/lib/__tests__/
├── chatHistoryStorage.test.ts
├── configPersistence.test.ts
├── imageGenClient.test.ts
├── llmClient.test.ts      ✅ Good coverage
├── logPlugin.test.ts
├── logger.test.ts
└── vibeContainerMock.test.ts

Missing tests for:

  • appRegistry.ts
  • action.ts
  • windowManager.ts
  • characterManager.ts
  • memoryManager.ts
  • fileTools.ts
  • All components

6.2 E2E Browser Coverage

// playwright.config.ts - Only Chromium configured
projects: [
  {
    name: 'chromium',
    use: { ...devices['Desktop Chrome'] },
  },
],

Recommendations

6.3 Add Coverage Thresholds

// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'happy-dom',
    include: ['src/**/*.{test,spec}.{ts,tsx}'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      thresholds: {
        global: {
          branches: 70,
          functions: 80,
          lines: 80,
          statements: 80,
        },
        // Per-file thresholds for critical modules
        'src/lib/llmClient.ts': {
          branches: 90,
          functions: 95,
          lines: 95,
        },
        'src/lib/action.ts': {
          branches: 85,
          functions: 90,
          lines: 90,
        },
      },
      exclude: [
        '**/*.d.ts',
        '**/*.config.ts',
        '**/mock/**',
        '**/*.config.*',
      ],
    },
  },
});

6.4 Add More Browser Testing

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [['html'], ['github']],
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 12'] },
    },
  ],
  webServer: {
    command: 'pnpm dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 30_000,
  },
});

6.5 Add Component Testing

pnpm add -D @testing-library/react @testing-library/jest-dom @testing-library/user-event
// Example: apps/webuiapps/src/components/__tests__/MessageList.test.tsx
import { render, screen } from '@testing-library/react';
import { MessageList } from '../MessageList';

describe('MessageList', () => {
  it('renders empty state when no messages', () => {
    render(<MessageList messages={[]} isLoading={false} />);
    expect(screen.getByText(/no messages yet/i)).toBeInTheDocument();
  });

  it('renders messages in order', () => {
    const messages = [
      { id: '1', role: 'user', content: 'Hello' },
      { id: '2', role: 'assistant', content: 'Hi there!' },
    ];
    render(<MessageList messages={messages} isLoading={false} />);
    expect(screen.getByText('Hello')).toBeInTheDocument();
    expect(screen.getByText('Hi there!')).toBeInTheDocument();
  });

  it('shows loading indicator when loading', () => {
    render(<MessageList messages={[]} isLoading={true} />);
    expect(screen.getByRole('progressbar')).toBeInTheDocument();
  });
});

6.6 Add Test Scripts

// package.json
{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "test:coverage": "vitest run --coverage",
    "test:ui": "vitest --ui",
    "test:e2e": "playwright test",
    "test:e2e:ui": "playwright test --ui",
    "test:e2e:debug": "playwright test --debug",
    "ci:test": "pnpm test:coverage && pnpm test:e2e"
  }
}

7. Dependency Optimization

Current State

Package Current Latest Status
React 18.2.0 18.3.1 ⚠️ Update
React DOM 18.2.0 18.3.1 ⚠️ Update
React Router 6.17.0 6.28.0 ⚠️ Update
i18next 23.11.4 24.x ⚠️ Update
framer-motion 12.34.0 Latest ✅ Current
Three.js 0.170.0 Latest ✅ Current

Issues

7.1 Duplicate Animation Libraries

// Both libraries serve similar purposes
"framer-motion": "^12.34.0",       // UI animations
"@react-spring/three": "^9.7.5",   // 3D animations
"@react-three/drei": "^9.122.0",   // Three.js helpers

Recommendations

7.2 Update Dependencies

# Update React ecosystem
pnpm update react react-dom @types/react @types/react-dom

# Update routing
pnpm update react-router-dom

# Update i18n
pnpm update i18next react-i18next

# Update testing
pnpm update -D @playwright/test vitest @vitest/coverage-v8

7.3 Consolidate Animation Libraries

Recommendation: Keep both but document usage guidelines

## Animation Library Usage

- **framer-motion**: Use for all UI animations (panels, modals, transitions)
- **@react-spring/three**: Use only for 3D scene animations (Three.js canvases)
- **@react-three/drei**: Use for Three.js utilities and helpers

Do not mix framer-motion with @react-spring for the same animation.

7.4 Add Dependency Audit

// package.json
{
  "scripts": {
    "deps:check": "npm-check-updates",
    "deps:update": "npm-check-updates -u && pnpm install",
    "deps:audit": "pnpm audit --audit-level=high"
  }
}
pnpm add -D npm-check-updates

8. Security Concerns

Current State

  • API keys in localStorage - LLM credentials stored client-side
  • Dev server file system access - Plugins read/write ~/.openroom/
  • No CSP headers
  • Unsafe HTML rendering - rehype-raw allows arbitrary HTML

Issues

8.1 Client-Side API Key Storage

// apps/webuiapps/src/lib/llmClient.ts
export async function saveConfig(config: LLMConfig): Promise<void> {
  localStorage.setItem(CONFIG_KEY, JSON.stringify(config)); // ❌ API key in localStorage
}

8.2 Unrestricted File System Access

// vite.config.ts - Dev server plugins
server.middlewares.use('/api/session-data', (req, res) => {
  // Reads/writes to ~/.openroom/sessions without authentication
  // In production, this could expose user data
});

8.3 No Content Security Policy

<!-- No CSP meta tag or headers -->
<meta http-equiv="Content-Security-Policy" content="..."> <!-- Missing -->

8.4 Potentially Unsafe Markdown

// Using rehype-raw allows arbitrary HTML in markdown
import ReactMarkdown from 'react-markdown';
import rehypeRaw from 'rehype-raw';

// This allows XSS if markdown content is user-controlled
<ReactMarkdown rehypePlugins={[rehypeRaw]}>
  {content}
</ReactMarkdown>

Recommendations

8.5 Secure API Key Storage

Option A: Environment Variables (Development Only)

// .env.local (gitignored)
VITE_LLM_API_KEY=your-api-key-here

// llmClient.ts
const config: LLMConfig = {
  provider: 'openai',
  apiKey: import.meta.env.VITE_LLM_API_KEY,
  // ...
};

Option B: Backend Proxy (Production)

// Move API calls to backend
// Frontend sends to your backend, backend forwards to LLM provider
export async function chat(messages: ChatMessage[], tools: ToolDef[]): Promise<LLMResponse> {
  const response = await fetch('/api/chat', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ messages, tools }),
    credentials: 'include', // Session-based auth
  });
  return response.json();
}

8.6 Add Content Security Policy

<!-- apps/webuiapps/index.html -->
<head>
  <meta 
    http-equiv="Content-Security-Policy" 
    content="
      default-src 'self';
      script-src 'self' 'unsafe-inline' 'unsafe-eval';
      style-src 'self' 'unsafe-inline';
      img-src 'self' data: https:;
      font-src 'self' data:;
      connect-src 'self' http://localhost:* https://api.*.com;
      media-src 'self' blob: data:;
    "
  >
</head>
// vite.config.ts - Add CSP headers in preview
import type { Plugin } from 'vite';

function cspPlugin(): Plugin {
  return {
    name: 'csp-headers',
    configurePreviewServer(server) {
      server.middlewares.use((req, res, next) => {
        res.setHeader('Content-Security-Policy', `
          default-src 'self';
          script-src 'self' 'unsafe-inline';
          style-src 'self' 'unsafe-inline';
          img-src 'self' data: https:;
          connect-src 'self' https://api.*.com;
        `.replace(/\s+/g, ' ').trim());
        next();
      });
    },
  };
}

8.7 Sanitize Markdown Content

pnpm add dompurify
pnpm add -D @types/dompurify
// Create sanitized markdown component
import DOMPurify from 'dompurify';
import ReactMarkdown from 'react-markdown';

interface SafeMarkdownProps {
  content: string;
}

export const SafeMarkdown: React.FC<SafeMarkdownProps> = ({ content }) => {
  // Sanitize before rendering
  const sanitized = DOMPurify.sanitize(content, {
    ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'code', 'pre', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'blockquote'],
    ALLOWED_ATTR: [],
  });

  return <ReactMarkdown>{sanitized}</ReactMarkdown>;
};

8.8 Secure Dev Server Plugins

// vite.config.ts - Add authentication check
function sessionDataPlugin(): Plugin {
  return {
    name: 'session-data',
    configureServer(server) {
      server.middlewares.use('/api/session-data', (req, res, next) => {
        // Add CORS restrictions
        res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000');
        res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE');
        res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
        
        // Validate path parameter to prevent directory traversal
        const url = new URL(req.url || '', 'http://localhost');
        const relPath = url.searchParams.get('path') || '';
        
        // Block path traversal attempts
        if (relPath.includes('..') || relPath.includes('\\')) {
          res.writeHead(400);
          res.end(JSON.stringify({ error: 'Invalid path' }));
          return;
        }
        
        next();
      });
    },
  };
}

9. i18n Implementation

Current State

  • Inconsistent structure - Some apps have i18n/, some don't
  • No fallback handling - Missing keys show raw key names
  • No translation completeness tests

Issues

9.1 Inconsistent Translation Files

apps/webuiapps/src/pages/
├── MusicApp/i18n/     ✅ Has translations
├── Diary/i18n/        ✅ Has translations
├── Chess/i18n/        ? Check existence
├── Email/i18n/        ? Check existence
└── ...

9.2 No Missing Key Handler

// i18next configuration
i18n.use(initReactI18next).init({
  // Missing: missingKeyHandler
});

Recommendations

9.3 Standardize i18n Structure

// apps/webuiapps/src/i18n/config.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import { logger } from '@/lib/logger';

i18n.use(initReactI18next).init({
  resources: {
    en: {
      translation: {
        // Common translations
      },
    },
    zh: {
      translation: {
        // Common translations
      },
    },
  },
  lng: 'en',
  fallbackLng: 'en',
  ns: ['common', 'errors', 'apps'],
  defaultNS: 'common',
  interpolation: {
    escapeValue: false,
  },
  react: {
    useSuspense: false,
  },
  missingKeyHandler: (lng, ns, key, fallbackValue) => {
    logger.warn('i18n', `Missing translation key: ${ns}:${key}`);
    return fallbackValue || key;
  },
  saveMissing: true,
  parseMissingKeyHandler: (key) => {
    logger.warn('i18n', `Missing key parsed: ${key}`);
    return key;
  },
});

export default i18n;

9.4 Add Translation Completeness Test

// apps/webuiapps/src/i18n/__tests__/completeness.test.ts
import { describe, it, expect } from 'vitest';
import en from '../en';
import zh from '../zh';

function flattenKeys(obj: Record<string, unknown>, prefix = ''): string[] {
  return Object.entries(obj).reduce<string[]>((acc, [key, value]) => {
    const fullKey = prefix ? `${prefix}.${key}` : key;
    if (typeof value === 'object' && value !== null) {
      acc.push(...flattenKeys(value as Record<string, unknown>, fullKey));
    } else {
      acc.push(fullKey);
    }
    return acc;
  }, []);
}

describe('i18n completeness', () => {
  it('has same keys in all languages', () => {
    const enKeys = flattenKeys(en).sort();
    const zhKeys = flattenKeys(zh).sort();
    
    const missingInZh = enKeys.filter((key) => !zhKeys.includes(key));
    const extraInZh = zhKeys.filter((key) => !enKeys.includes(key));
    
    expect(missingInZh).toEqual([]);
    expect(extraInZh).toEqual([]);
  });

  it('has no empty string values', () => {
    const flattenValues = (obj: Record<string, unknown>): string[] => {
      return Object.values(obj).reduce<string[]>((acc, value) => {
        if (typeof value === 'object' && value !== null) {
          acc.push(...flattenValues(value as Record<string, unknown>));
        } else {
          acc.push(String(value));
        }
        return acc;
      }, []);
    };

    const enValues = flattenValues(en);
    const zhValues = flattenValues(zh);

    enValues.forEach((value, index) => {
      expect(value).not.toBe('');
    });
    
    zhValues.forEach((value, index) => {
      expect(value).not.toBe('');
    });
  });
});

9.5 Add i18n Linting Script

# script/check-i18n.js
import { readFileSync } from 'fs';
import { glob } from 'glob';

const translationFiles = glob.sync('src/pages/*/i18n/*.ts');
// Check for missing keys, empty values, etc.

10. File System & Storage

Current State

  • Silent failures - diskStorage.ts returns null on errors
  • No quota management - IndexedDB can fill up
  • No migration strategy - Schema changes break existing data
  • Hardcoded paths - ~/.openroom/sessions not configurable

Issues

10.1 Silent Error Handling

// apps/webuiapps/src/lib/diskStorage.ts
export async function getFile(filePath: string): Promise<unknown> {
  try {
    const res = await fetch(apiUrl(filePath));
    if (!res.ok) return null;  // ❌ Silent failure
    // ...
  } catch (e) {
    console.warn('[diskStorage] getFile failed:', e);  // ❌ Just warns
    return null;  // ❌ Returns null without context
  }
}

10.2 No Storage Quota Check

// No quota monitoring
// IndexedDB has ~50% of disk space limit but no checks

Recommendations

10.3 Add Error Handling with Context

// apps/webuiapps/src/lib/diskStorage.ts
export class StorageError extends Error {
  constructor(
    message: string,
    public operation: string,
    public filePath: string,
    public cause?: unknown,
  ) {
    super(message);
    this.name = 'StorageError';
  }
}

export async function getFile(filePath: string): Promise<unknown> {
  try {
    const res = await fetch(apiUrl(filePath));
    if (!res.ok) {
      throw new StorageError(
        `Failed to get file: ${res.status} ${res.statusText}`,
        'getFile',
        filePath,
      );
    }
    const text = await res.text();
    try {
      return JSON.parse(text);
    } catch {
      return text;
    }
  } catch (e) {
    if (e instanceof StorageError) {
      logger.error('Storage', e.message, { operation: e.operation, path: e.filePath });
      throw e;
    }
    const error = new StorageError(
      e instanceof Error ? e.message : 'Unknown error',
      'getFile',
      filePath,
      e,
    );
    logger.error('Storage', 'getFile failed', { path: filePath, error });
    throw error;
  }
}

10.4 Add Storage Quota Monitoring

// apps/webuiapps/src/lib/storageQuota.ts
import { logger } from './logger';

export interface StorageEstimate {
  usage: number;
  quota: number;
  percentUsed: number;
}

export async function checkStorageQuota(): Promise<StorageEstimate> {
  if (!navigator.storage?.estimate) {
    return { usage: 0, quota: 0, percentUsed: 0 };
  }

  const estimate = await navigator.storage.estimate();
  const usage = estimate.usage || 0;
  const quota = estimate.quota || 0;
  const percentUsed = quota > 0 ? (usage / quota) * 100 : 0;

  logger.info('Storage', 'Quota check', {
    usage: `${(usage / 1024 / 1024).toFixed(2)} MB`,
    quota: `${(quota / 1024 / 1024).toFixed(2)} MB`,
    percentUsed: `${percentUsed.toFixed(1)}%`,
  });

  // Warn if over 80% used
  if (percentUsed > 80) {
    logger.warn('Storage', 'Storage quota nearly exceeded', { percentUsed });
  }

  return { usage, quota, percentUsed };
}

export async function persistStorage(): Promise<boolean> {
  if (!navigator.storage?.persist) {
    return false;
  }

  const persisted = await navigator.storage.persist();
  logger.info('Storage', 'Persistence request', { persisted });
  return persisted;
}

10.5 Add Data Migration System

// apps/webuiapps/src/lib/migrations.ts
import { logger } from './logger';

interface Migration {
  version: number;
  description: string;
  migrate: () => Promise<void>;
}

const migrations: Migration[] = [
  {
    version: 1,
    description: 'Initial migration',
    migrate: async () => {
      // Initial setup
    },
  },
  {
    version: 2,
    description: 'Migrate chat history to new format',
    migrate: async () => {
      // Migration logic
    },
  },
];

export async function runMigrations(): Promise<void> {
  const currentVersion = parseInt(localStorage.getItem('schema_version') || '0', 10);
  
  for (const migration of migrations) {
    if (migration.version > currentVersion) {
      logger.info('Migration', `Running migration ${migration.version}: ${migration.description}`);
      try {
        await migration.migrate();
        localStorage.setItem('schema_version', migration.version.toString());
      } catch (error) {
        logger.error('Migration', `Migration ${migration.version} failed`, error);
        throw error;
      }
    }
  }
}

10.6 Make Session Path Configurable

// apps/webuiapps/src/lib/sessionPath.ts
const DEFAULT_SESSIONS_DIR = '~/.openroom/sessions';

let customSessionsDir: string | null = null;

export function setSessionsDir(path: string): void {
  customSessionsDir = path;
}

export function getSessionsDir(): string {
  return customSessionsDir || DEFAULT_SESSIONS_DIR;
}

11. LLM Client Optimization

Current State

  • No request caching - Same prompts sent repeatedly
  • No retry logic - Network errors fail immediately
  • No timeout - LLM calls can hang indefinitely
  • Verbose logging - 157 console statements

Issues

11.1 No Caching

// Same system prompt sent with every request
// No caching of tool definitions
// No caching of previous responses

11.2 No Retry Logic

// apps/webuiapps/src/lib/llmClient.ts
const res = await fetch('/api/llm-proxy', { /* ... */ });
// Single attempt, no retry on failure

11.3 No Request Timeout

// fetch() has no built-in timeout
// LLM calls can hang for minutes

Recommendations

11.4 Add Request Caching

// apps/webuiapps/src/lib/llmCache.ts
import { ChatMessage, ToolCall } from './llmClient';

interface CacheEntry {
  messagesHash: string;
  response: { content: string; toolCalls: ToolCall[] };
  timestamp: number;
}

const cache = new Map<string, CacheEntry>();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes

function hashMessages(messages: ChatMessage[]): string {
  return JSON.stringify(messages);
}

export function getCachedResponse(messages: ChatMessage[]): { content: string; toolCalls: ToolCall[] } | null {
  const hash = hashMessages(messages);
  const entry = cache.get(hash);
  
  if (!entry) return null;
  if (Date.now() - entry.timestamp > CACHE_TTL) {
    cache.delete(hash);
    return null;
  }
  
  return entry.response;
}

export function cacheResponse(
  messages: ChatMessage[],
  response: { content: string; toolCalls: ToolCall[] },
): void {
  const hash = hashMessages(messages);
  cache.set(hash, {
    messagesHash: hash,
    response,
    timestamp: Date.now(),
  });
}

export function clearCache(): void {
  cache.clear();
}

11.5 Add Retry with Exponential Backoff

// apps/webuiapps/src/lib/llmClient.ts
async function fetchWithRetry(
  url: string,
  options: RequestInit,
  maxRetries = 3,
): Promise<Response> {
  let lastError: Error | null = null;
  
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url, options);
      
      // Don't retry on client errors (4xx)
      if (response.status >= 400 && response.status < 500) {
        return response;
      }
      
      if (response.ok) {
        return response;
      }
      
      lastError = new Error(`HTTP ${response.status}`);
    } catch (error) {
      lastError = error instanceof Error ? error : new Error('Unknown error');
    }
    
    // Wait before retry (exponential backoff)
    if (i < maxRetries - 1) {
      const delay = Math.pow(2, i) * 1000; // 1s, 2s, 4s
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }
  
  throw lastError;
}

// Usage
const res = await fetchWithRetry('/api/llm-proxy', {
  method: 'POST',
  // ...
});

11.6 Add Request Timeout

// apps/webuiapps/src/lib/llmClient.ts
async function fetchWithTimeout(
  url: string,
  options: RequestInit,
  timeoutMs = 60000, // 60 seconds
): Promise<Response> {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
  
  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal,
    });
    return response;
  } finally {
    clearTimeout(timeoutId);
  }
}

// Usage with both retry and timeout
const res = await fetchWithRetry('/api/llm-proxy', {
  method: 'POST',
  signal: AbortSignal.timeout(60000), // Alternative: built-in timeout
});

11.7 Configurable Logging

// apps/webuiapps/src/lib/logger.ts
export enum LogLevel {
  DEBUG = 0,
  INFO = 1,
  WARN = 2,
  ERROR = 3,
  NONE = 4,
}

let currentLevel = LogLevel.INFO;

export function setLogLevel(level: LogLevel): void {
  currentLevel = level;
}

export function getLogLevel(): LogLevel {
  return currentLevel;
}

// In llmClient.ts
if (getLogLevel() <= LogLevel.DEBUG) {
  logger.debug('LLM', 'Request details:', { targetUrl, model, messageCount });
}

12. Monorepo Structure

Current State

  • Single app: Only apps/webuiapps exists
  • Stub package: packages/vibe-container is a mock
  • Minimal Turborepo config: Only build, dev, clean tasks

Issues

12.1 Underutilized Monorepo

OpenRoom/
├── apps/
│   └── webuiapps/    # Only app
├── packages/
│   └── vibe-container/  # Stub/mock

12.2 Limited Turborepo Pipeline

// turbo.json
{
  "tasks": {
    "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
    "dev": { "cache": false, "persistent": true },
    "clean": { "cache": false }
  }
}

Recommendations

12.3 Extract Shared UI Library

# Create shared component library
mkdir -p packages/ui/src/components
// packages/ui/src/components/Button.tsx
import React from 'react';
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
}

export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant = 'primary', size = 'md', ...props }, ref) => {
    return (
      <button
        ref={ref}
        className={twMerge(
          clsx(
            'inline-flex items-center justify-center rounded-md font-medium transition-colors',
            {
              'bg-primary text-primary-foreground hover:bg-primary/90': variant === 'primary',
              'bg-secondary text-secondary-foreground hover:bg-secondary/80': variant === 'secondary',
              'hover:bg-accent hover:text-accent-foreground': variant === 'ghost',
              'h-8 px-3 text-sm': size === 'sm',
              'h-10 px-4 text-base': size === 'md',
              'h-12 px-6 text-lg': size === 'lg',
            },
            className,
          ),
        )}
        {...props}
      />
    );
  },
);

Button.displayName = 'Button';

12.4 Enhance Turborepo Configuration

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "globalEnv": ["NODE_ENV", "ANALYZE", "CI"],
  "tasks": {
    "build": {
      "dependsOn": ["^build", "lint", "test"],
      "outputs": ["dist/**", ".next/**"],
      "env": ["CDN_PREFIX", "VITE_*", "SENTRY_*"],
      "inputs": ["src/**", "*.ts", "*.tsx", "tsconfig.json"]
    },
    "lint": {
      "dependsOn": ["^build"],
      "outputs": [],
      "inputs": ["src/**", "*.ts", "*.tsx"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"],
      "inputs": ["src/**", "*.test.ts", "*.test.tsx"]
    },
    "test:watch": {
      "cache": false,
      "persistent": true
    },
    "dev": {
      "cache": false,
      "persistent": true,
      "inputs": ["src/**", "*.ts", "*.tsx"]
    },
    "clean": {
      "cache": false,
      "outputs": []
    },
    "typecheck": {
      "dependsOn": ["^build"],
      "outputs": [],
      "inputs": ["src/**", "*.ts", "*.tsx", "tsconfig.json"]
    }
  }
}

12.5 Add Type Checking Task

// package.json
{
  "scripts": {
    "typecheck": "tsc --noEmit",
    "ci:check": "turbo run lint test typecheck build"
  }
}

13. Accessibility (a11y)

Current State

  • No aria-labels on icon buttons
  • No keyboard navigation for draggable windows
  • No focus management for modals
  • Color contrast unverified

Issues

13.1 Missing ARIA Attributes

// apps/webuiapps/src/pages/MusicApp/index.tsx
<button onClick={handlePlay}>
  <Play size={20} />  {/*  No aria-label */}
</button>

13.2 No Keyboard Support

// Windows can only be moved with mouse
// No Escape key to minimize
// No Tab navigation between windows

Recommendations

13.3 Add ARIA Labels

// apps/webuiapps/src/pages/MusicApp/index.tsx
import { useTranslation } from 'react-i18next';

const { t } = useTranslation('musicApp');

<button 
  onClick={handlePlay}
  aria-label={t('play')}
  title={t('play')}
>
  <Play size={20} aria-hidden />
</button>

<button
  onClick={() => onSelectPlaylist(playlist.id)}
  aria-label={t('selectPlaylist', { name: playlist.name })}
  aria-current={currentPlaylistId === playlist.id ? 'true' : 'false'}
>
  <ListMusic size={18} aria-hidden />
  {playlist.name}
</button>

13.4 Add Keyboard Navigation

// apps/webuiapps/src/components/Shell/index.tsx
useEffect(() => {
  const handleKeyDown = (e: KeyboardEvent) => {
    // Escape to minimize focused window
    if (e.key === 'Escape') {
      const focused = windows.find((w) => w.zIndex === Math.max(...windows.map((w) => w.zIndex)));
      if (focused) {
        minimizeWindow(focused.appId);
      }
    }

    // Alt+Tab to cycle through windows
    if (e.altKey && e.key === 'Tab') {
      e.preventDefault();
      const sortedWindows = [...windows].sort((a, b) => b.zIndex - a.zIndex);
      const currentFocusedIndex = sortedWindows.findIndex((w) => !w.minimized);
      const nextWindow = sortedWindows[(currentFocusedIndex + 1) % sortedWindows.length];
      if (nextWindow) {
        focusWindow(nextWindow.appId);
      }
    }

    // Arrow keys to move focused window (when holding Shift)
    if (e.shiftKey && ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
      e.preventDefault();
      const focused = windows.find((w) => w.zIndex === Math.max(...windows.map((w) => w.zIndex)));
      if (focused) {
        const step = 10;
        const deltas: Record<string, { x: number; y: number }> = {
          ArrowUp: { x: 0, y: -step },
          ArrowDown: { x: 0, y: step },
          ArrowLeft: { x: -step, y: 0 },
          ArrowRight: { x: step, y: 0 },
        };
        const delta = deltas[e.key];
        moveWindow(focused.appId, focused.x + delta.x, focused.y + delta.y);
      }
    }
  };

  window.addEventListener('keydown', handleKeyDown);
  return () => window.removeEventListener('keydown', handleKeyDown);
}, [windows]);

13.5 Add Focus Management

// apps/webuiapps/src/components/ChatPanel/index.tsx
useEffect(() => {
  if (visible) {
    // Focus the input when panel opens
    inputRef.current?.focus();
    
    // Trap focus inside panel
    const previousActiveElement = document.activeElement;
    const panel = panelRef.current;
    
    const handleFocus = (e: FocusEvent) => {
      if (panel && !panel.contains(e.target as Node)) {
        inputRef.current?.focus();
      }
    };
    
    document.addEventListener('focus', handleFocus, true);
    return () => {
      document.removeEventListener('focus', handleFocus, true);
      (previousActiveElement as HTMLElement)?.focus();
    };
  }
}, [visible]);

13.6 Add Accessibility Testing

pnpm add -D axe-core @axe-core/playwright
// e2e/accessibility.test.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test.describe('Accessibility', () => {
  test('homepage should not have accessibility violations', async ({ page }) => {
    await page.goto('/');
    
    const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
    
    expect(accessibilityScanResults.violations).toEqual([]);
  });

  test('chat panel should be accessible', async ({ page }) => {
    await page.goto('/');
    
    // Open chat panel
    await page.click('[data-testid="chat-panel-toggle"]');
    
    const accessibilityScanResults = await new AxeBuilder({ page })
      .include('[data-testid="chat-panel"]')
      .analyze();
    
    expect(accessibilityScanResults.violations).toEqual([]);
  });
});

13.7 Color Contrast Check

Add to documentation:

## Color Contrast Requirements

All text must meet WCAG AA standards:
- Normal text: 4.5:1 contrast ratio
- Large text (18px+ or 14px+ bold): 3:1 contrast ratio
- UI components: 3:1 contrast ratio

Use tools to verify:
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
- [Chrome DevTools Accessibility panel](https://developer.chrome.com/docs/devtools/accessibility/)

14. Documentation & DX

Current State

  • CLAUDE.md: Anthropic-specific workflow guide
  • No Storybook: Hard to develop components in isolation
  • No JSDoc: Lib functions lack documentation
  • Manual CHANGELOG: Not auto-generated

Recommendations

14.1 Add Storybook

pnpm add -D @storybook/react @storybook/addon-essentials @storybook/addon-a11y @storybook/addon-docs storybook
npx storybook init
// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
  stories: ['../src/**/*.stories.@(ts|tsx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-a11y',
    '@storybook/addon-docs',
  ],
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
  docs: {
    autodocs: 'tag',
  },
};

export default config;
// Example story: src/components/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
  parameters: {
    layout: 'centered',
  },
  tags: ['autodocs'],
  argTypes: {
    variant: {
      control: 'radio',
      options: ['primary', 'secondary', 'ghost'],
    },
    size: {
      control: 'radio',
      options: ['sm', 'md', 'lg'],
    },
  },
};

export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = {
  args: {
    children: 'Button',
    variant: 'primary',
  },
};

export const Secondary: Story = {
  args: {
    children: 'Button',
    variant: 'secondary',
  },
};

export const Ghost: Story = {
  args: {
    children: 'Button',
    variant: 'ghost',
  },
};

export const Disabled: Story = {
  args: {
    children: 'Button',
    disabled: true,
  },
};

14.2 Add JSDoc Comments

// apps/webuiapps/src/lib/llmClient.ts

/**
 * Send a chat message to the LLM with optional tool calls.
 *
 * @param messages - Array of chat messages in the conversation
 * @param tools - Array of tool definitions available to the LLM
 * @param config - LLM configuration (provider, API key, model, etc.)
 * @returns Promise resolving to LLM response with content and tool calls
 *
 * @example
 * ```typescript
 * const response = await chat(
 *   [{ role: 'user', content: 'Hello' }],
 *   [getWeatherTool],
 *   openaiConfig
 * );
 * console.log(response.content); // "Hello! How can I help?"
 * ```
 *
 * @throws {Error} When the LLM API returns an error response
 */
export async function chat(
  messages: ChatMessage[],
  tools: ToolDef[],
  config: LLMConfig,
): Promise<LLMResponse> {
  // ...
}

14.3 Auto-Generate CHANGELOG

pnpm add -D conventional-changelog-cli
// package.json
{
  "scripts": {
    "changelog:generate": "conventional-changelog -p angular -i CHANGELOG.md -s",
    "release": "standard-version"
  }
}
// .versionrc.json
{
  "types": [
    { "type": "feat", "section": "Features" },
    { "type": "fix", "section": "Bug Fixes" },
    { "type": "perf", "section": "Performance Improvements" },
    { "type": "revert", "section": "Reverts" },
    { "type": "docs", "section": "Documentation" },
    { "type": "style", "section": "Styles" },
    { "type": "refactor", "section": "Code Refactoring" },
    { "type": "test", "section": "Tests" },
    { "type": "chore", "hidden": true }
  ]
}

14.4 Add Contributing Guidelines

# Contributing to OpenRoom

## Development Setup

1. Clone the repository
2. Install dependencies: `pnpm install`
3. Copy environment file: `cp apps/webuiapps/.env.example apps/webuiapps/.env`
4. Start dev server: `pnpm dev`

## Code Style

- Follow existing code conventions
- Use TypeScript strict mode
- Add JSDoc comments for public APIs
- Write tests for new features

## Pull Request Process

1. Create a feature branch
2. Make changes and add tests
3. Run linting: `pnpm lint`
4. Run tests: `pnpm test`
5. Update documentation
6. Submit PR with clear description

## Testing Requirements

- Unit tests: >90% coverage for changed code
- E2E tests: Cover primary user flows
- Accessibility: No new violations

Priority Summary

🔴 Critical Priority (Address Immediately)

# Category Issue Impact Effort Timeline
1 Security API keys in localStorage Critical Medium 1 week
2 Security No CSP headers High Low 2 days
3 Security Unsafe HTML rendering High Low 1 day
4 Type Safety as any usage Medium Low 1 day

🟡 High Priority (Address This Sprint)

# Category Issue Impact Effort Timeline
5 Build Outdated Vite version Medium Medium 1 week
6 Build No code splitting High Medium 1 week
7 Testing Low coverage High High 2 weeks
8 React Monolithic ChatPanel Medium High 2 weeks
9 TypeScript Outdated config Medium Low 2 days
10 ESLint Lenient rules Medium Low 1 day

🟢 Medium Priority (Address This Month)

# Category Issue Impact Effort Timeline
11 State Mutable module state Medium Medium 1 week
12 LLM No retry/caching Medium Medium 1 week
13 Storage Silent failures Medium Medium 1 week
14 i18n Inconsistent structure Low Medium 1 week
15 a11y Missing ARIA labels Medium Medium 1 week
16 a11y No keyboard navigation Medium High 2 weeks

🔵 Low Priority (Backlog)

# Category Issue Impact Effort Timeline
17 Dependencies Update React ecosystem Low Low 1 day
18 Monorepo Extract shared UI lib Low High 2 weeks
19 DX Add Storybook Low Medium 1 week
20 DX Add JSDoc Low High Ongoing
21 DX Auto-generate CHANGELOG Low Low 1 day

Quick Wins (Can Be Done in 1 Day)

  1. ✅ Fix as any type assertions in action.ts
  2. ✅ Update ESLint rules for stricter type checking
  3. ✅ Add CSP meta tag to index.html
  4. ✅ Replace console.log with logger module
  5. ✅ Add aria-labels to icon buttons
  6. ✅ Add JSDoc to public API functions
  7. ✅ Configure Vitest coverage thresholds
  8. ✅ Add retry logic to LLM client

Implementation Roadmap

Phase 1: Security & Stability (Week 1-2)

  • Secure API key storage
  • Add CSP headers
  • Sanitize markdown content
  • Fix type safety issues
  • Add error handling to storage

Phase 2: Performance (Week 3-4)

  • Upgrade Vite and dependencies
  • Implement code splitting
  • Add LLM caching
  • Optimize ChatPanel component
  • Add retry with backoff

Phase 3: Quality (Week 5-6)

  • Increase test coverage to 80%
  • Add E2E tests for all browsers
  • Fix accessibility violations
  • Add keyboard navigation
  • Implement state management with Zustand

Phase 4: DX & Documentation (Week 7-8)

  • Set up Storybook
  • Add JSDoc comments
  • Auto-generate CHANGELOG
  • Improve i18n structure
  • Create migration system

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions