-
Notifications
You must be signed in to change notification settings - Fork 80
Closed
Labels
enhancementNew feature or requestNew feature or request
Description
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
- Build & Bundle Optimization
- TypeScript Configuration
- Code Quality & Type Safety
- React Performance
- State Management
- Testing Coverage
- Dependency Optimization
- Security Concerns
- i18n Implementation
- File System & Storage
- LLM Client Optimization
- Monorepo Structure
- Accessibility (a11y)
- Documentation & DX
- Priority Summary
1. Build & Bundle Optimization
Current State
- Vite Version: 4.4.5 (outdated, latest is 6.x)
- Legacy Plugin:
@vitejs/plugin-legacyadds 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 required1.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 anyinaction.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 checking3.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, configurable3.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
CharacterAvataruses 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 rendering4.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.tsx4.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]); // Dependencies4.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]); // Dependency5. State Management
Current State
- Mixed patterns: Context + Reducer, localStorage, IndexedDB, file system
- No devtools: Hard to debug state changes
- Mutable module state:
windowManager.tsuses 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.tsaction.tswindowManager.tscharacterManager.tsmemoryManager.tsfileTools.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 | |
| React DOM | 18.2.0 | 18.3.1 | |
| React Router | 6.17.0 | 6.28.0 | |
| i18next | 23.11.4 | 24.x | |
| 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 helpersRecommendations
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-v87.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-updates8. 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-rawallows 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.tsreturnsnullon errors - No quota management - IndexedDB can fill up
- No migration strategy - Schema changes break existing data
- Hardcoded paths -
~/.openroom/sessionsnot 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 checksRecommendations
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 responses11.2 No Retry Logic
// apps/webuiapps/src/lib/llmClient.ts
const res = await fetch('/api/llm-proxy', { /* ... */ });
// Single attempt, no retry on failure11.3 No Request Timeout
// fetch() has no built-in timeout
// LLM calls can hang for minutesRecommendations
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/webuiappsexists - Stub package:
packages/vibe-containeris 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 windowsRecommendations
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 violationsPriority 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)
- ✅ Fix
as anytype assertions inaction.ts - ✅ Update ESLint rules for stricter type checking
- ✅ Add CSP meta tag to
index.html - ✅ Replace
console.logwithloggermodule - ✅ Add aria-labels to icon buttons
- ✅ Add JSDoc to public API functions
- ✅ Configure Vitest coverage thresholds
- ✅ 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
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request