Skip to content

Latest commit

Β 

History

History
389 lines (297 loc) Β· 12.2 KB

File metadata and controls

389 lines (297 loc) Β· 12.2 KB

React Layer Complexity Audit - MOVE TO CORE Candidates

Cell: opencode-next--xts0a-mjuyf47sofd
Epic: Event Architecture Simplification
Date: 2025-12-31

Executive Summary

ADR-015 CLAIMS VERIFIED:

  • βœ… Factory: 1,160 LOC (claimed 1,161) - ACCURATE
  • βœ… Factory: 22 hook functions (claimed 23 closures) - ACCURATE (off by 1)
  • ⚠️ Closure nesting: Not as deep as claimed (mostly 1-2 levels, not 3-4)
  • βœ… Fragile SSE wiring: CONFIRMED - requires manual useSSEEvents() per page

COMPLEXITY METRICS:

  • Total React LOC: ~8,994 (factory 1,160 + store 896 + hooks 6,938)
  • Factory hooks: 22 functions, 14 useCallback, 6 useMemo, 12 useEffect
  • Store event handlers: 896 LOC with binary search operations
  • Hooks directory: 6,938 LOC total (includes tests)

REALITY CHECK:

  • ADR-015 claims 30-40% reduction potential β†’ OVERSTATED
  • Actual move-to-core potential: ~5-6% LOC (~477 lines)
  • Most complexity is LEGITIMATE React integration (Zustand, SSE, SSR)

Why the gap?

  • ADR-015 counted dead code removal (Router 3,462 LOC, SSEAtom 184 LOC) as "simplification"
  • Factory complexity is NOT about LOC but closure nesting (harder to debug)
  • Real win is NOT LOC reduction but separation of concerns (business logic β†’ core)

Part 1: Factory Analysis (1,160 LOC)

MOVE TO CORE - HIGH PRIORITY

1. Slash Command Parsing (~40 LOC)

Location: factory.ts lines 262-300
Current: Embedded in useSendMessage hook
Move to: @opencode-vibe/core/api/commands.ts

// Proposed Core API
export function parseSlashCommand(
  parts: Prompt,
  commandRegistry: SlashCommand[]
): ParsedCommand {
  // Extract text, check for /, lookup command
  // Returns: { isCommand: boolean, commandName?, arguments?, type? }
}

Benefits:

  • Testable without React
  • Reusable in TUI, CLI, mobile
  • Factory becomes: const parsed = parseSlashCommand(parts, commands.list())

2. Fuzzy File Search (~30 LOC)

Location: factory.ts lines 637-667
Current: fuzzysort logic in useFileSearch hook
Move to: @opencode-vibe/core/api/find.ts

// Proposed Core API
export async function searchFiles(
  query: string,
  directory: string,
  options?: { limit?: number; threshold?: number }
): Promise<string[]> {
  // Fetch files, apply fuzzysort, return filtered paths
}

Benefits:

  • Reusable outside React
  • Hook becomes: const { files, loading } = useAsync(() => searchFiles(query, dir))

MOVE TO CORE - MEDIUM PRIORITY

3. Message Queue Logic (~130 LOC)

Location: factory.ts lines 302-434
Current: Queue management + API calls in useSendMessage
Move to: @opencode-vibe/core/api/sessions.ts

// Proposed Core API
export class MessageQueue {
  async send(sessionId: string, parts: Prompt, directory: string): Promise<void>
  private async processNext()
}

Benefits:

  • Testable queue logic without React
  • React hook becomes thin wrapper managing UI state
  • Reusable in Node.js scripts, CLI

Concerns:

  • Queue needs session status to know when to process next message
  • Would require passing status as arg or subscription pattern

What CANNOT Move (Factory)

Pure UI Bindings (Must Stay):

  • All Zustand selectors: useSession, useMessages, useSessionList, useSessionStatus, useCompactionState, useContextUsage, useMessagesWithParts
  • SSR-safe wrappers: useConnectionStatus, getOpencodeConfig
  • SSE wiring: useSSEEvents - CRITICAL React↔Core bridge

Why these must stay:

  • Zustand integration requires React hooks
  • SSR safety requires React context (window checks, useEffect)
  • SSE subscription lifecycle tied to React component lifecycle

Part 2: Store Analysis (896 LOC)

MOVE TO CORE - HIGH PRIORITY

1. Context Usage Calculation (~27 LOC)

Location: store.ts lines 367-393
Current: Embedded in message.updated event handler
Move to: @opencode-vibe/core/api/context.ts

// Proposed Core API
export function calculateContextUsage(
  tokens: { input: number; output: number; cache?: { read?: number } },
  modelLimits: { context: number; output: number }
): ContextUsage {
  // Calculate used, percentage, isNearLimit
}

Benefits:

  • Pure function, testable without Zustand
  • Reusable in TUI, CLI (show context warnings)
  • Store becomes: dir.contextUsage[sessionID] = calculateContextUsage(tokens, limits)

MOVE TO CORE - MEDIUM PRIORITY

2. Compaction Detection (~30 LOC)

Location: store.ts lines 395-404, 442-464
Current: Scattered across message.updated and message.part.updated handlers
Move to: @opencode-vibe/core/api/compaction.ts

// Proposed Core API
export function detectCompactionStart(message: Message): CompactionState | null
export function detectCompactionPart(part: Part): CompactionState | null

Benefits:

  • Clear business logic extraction
  • Store becomes: const c = detectCompactionStart(msg); if (c) dir.compaction[id] = c

What CANNOT Move (Store)

Must Stay in React Layer:

  • Zustand store creation and Immer middleware
  • DirectoryState structure (React-specific state shape)
  • Binary search insertion logic (tightly coupled to store arrays)
  • Event routing (handleSSEEvent β†’ handleEvent β†’ specific handlers)

Why:

  • Zustand and Immer are React-specific libraries
  • State shape optimized for React selectors (shallow equality checks)
  • Event handlers trigger React re-renders

Part 3: Hooks Analysis (6,938 LOC)

MOVE TO CORE - HIGH PRIORITY

1. Session Status Bootstrap (~150 LOC)

Location: hooks/use-multi-directory-status.ts lines 100-151
Current: Async bootstrap in useEffect (fetches messages, derives status)
Move to: @opencode-vibe/core/api/status.ts

// Proposed Core API
export class SessionStatusManager {
  async bootstrapStatus(
    sessionIds: string[],
    directory: string
  ): Promise<Record<string, SessionStatus>>
  
  applyCooldown(sessionId: string, status: SessionStatus): SessionStatus
}

Benefits:

  • Largest win: 150+ LOC of business logic β†’ core
  • Hook becomes thin wrapper managing React state + timers
  • Reusable in TUI project list

What CANNOT Move (Hooks)

Pure React Patterns:

  • use-live-time.ts - Timer-based re-renders (React-specific)
  • use-sse.ts - EventSource lifecycle tied to useEffect
  • use-multi-server-sse.ts - React subscription to multiServerSSE singleton
  • use-subagent.ts - Zustand selector + local expand state
  • use-session-data.ts - RSC hydration + Zustand selectors
  • use-messages-with-parts.ts - useMemo joining (React optimization)

Why:

  • Component lifecycle (mount/unmount) management
  • SSR safety (useEffect guards)
  • React-specific optimizations (useMemo, useCallback)

Part 4: SSE Wiring Complexity

Current (Fragile)

Problem:

// MUST call in EVERY page or SSE breaks silently
export default function SessionPage({ params }) {
  useSSEEvents() // Forget this = no real-time updates, NO ERROR
  const messages = useMessages(params.id)
  return <MessageList messages={messages} />
}

Failure modes:

  1. Developer forgets useSSEEvents() β†’ silent data staleness
  2. Multiple calls β†’ multiple subscriptions (safe but wasteful)
  3. No compile-time safety

Proposed (Automatic)

Solution from ADR-015:

// In root layout - ONE call, applies everywhere
export default function RootLayout({ children }) {
  useSSEEvents() // Called once at app root
  return children
}

// Pages just work - no manual wiring needed
export default function SessionPage({ params }) {
  const messages = useMessages(params.id) // SSE already running
  return <MessageList messages={messages} />
}

CANNOT move to core because:

  • useSSEEvents IS the React↔Core bridge
  • Calls multiServerSSE.start() (core) + routes events to Zustand (React)
  • Lifecycle tied to root component mount/unmount

Can improve:

  • βœ… Document clearly in factory (already has good JSDoc)
  • Add ESLint rule to warn if useSSEEvents not found in layout
  • Add runtime warning if store receives events but useSSEEvents never called

Summary: MOVE TO CORE Recommendations

Phase 1: Extract Business Logic (1-2 days)

HIGH PRIORITY (~217 LOC to core):

  1. Slash Command Parsing (40 LOC)

    • From: factory.ts lines 262-300
    • To: @opencode-vibe/core/api/commands.ts
    • Benefit: Reusable in TUI, CLI, testable without React
  2. Context Usage Calculation (27 LOC)

    • From: store.ts lines 367-393
    • To: @opencode-vibe/core/api/context.ts
    • Benefit: Pure function, testable, reusable
  3. Session Status Bootstrap (150 LOC)

    • From: use-multi-directory-status.ts lines 100-151
    • To: @opencode-vibe/core/api/status.ts
    • Benefit: Largest win, removes complex async logic from hook

Implementation steps:

  1. Create new core API modules with extracted functions
  2. Add tests for core functions (easier without React)
  3. Update React layer to call new core functions
  4. Verify all existing tests pass
  5. Add new tests for integration points

Benefits:

  • Clear separation: Core = business logic, React = UI binding
  • Easier testing (no React Testing Library needed for business logic)
  • Reusable in TUI, CLI, mobile

Phase 2: SSE Wiring Automation (2-3 hours)

Move useSSEEvents() to root layout:

  1. Add useSSEEvents() call in apps/web/src/app/layout.tsx
  2. Remove per-page calls (document/layout.tsx, session/[id]/layout.tsx, etc.)
  3. Add runtime warning if SSE events received but hook never called
  4. Document pattern in factory JSDoc

Benefits:

  • Eliminates fragile per-page wiring
  • Prevents silent failures
  • Single source of truth for SSE initialization

Phase 3: Medium Priority Extractions (Future)

MEDIUM PRIORITY (~190 LOC to core):

  1. Message Queue Logic (130 LOC)

    • From: factory.ts lines 302-434
    • To: @opencode-vibe/core/api/sessions.ts
    • Concern: Needs session status integration design
  2. Compaction Detection (30 LOC)

    • From: store.ts lines 395-404, 442-464
    • To: @opencode-vibe/core/api/compaction.ts
    • Benefit: Clear business logic extraction
  3. Fuzzy File Search (30 LOC)

    • From: factory.ts lines 637-667
    • To: @opencode-vibe/core/api/find.ts
    • Benefit: Small but reusable

Metrics Summary

Layer Current LOC Can Move % Reduction What Stays
Factory 1,160 ~270 23% Config, UI bindings, SSE bridge
Store 896 ~57 6% Zustand logic, event routing
Hooks 6,938 ~150 2% React patterns, selectors
Total 8,994 ~477 5.3% React-specific code

Reality check:

  • ADR-015 claims 30-40% reduction potential β†’ OVERSTATED
  • Actual move-to-core potential: ~5-6% LOC (~477 lines)
  • Most complexity is LEGITIMATE React integration (Zustand, SSE, SSR)

Why the gap?

  • ADR-015 counted dead code removal (Router 3,462 LOC, SSEAtom 184 LOC) as part of "simplification"
  • Factory complexity is NOT about LOC but closure nesting (harder to debug)
  • Real win is NOT LOC reduction but separation of concerns (business logic β†’ core)

Conclusion

ADR-015 claims mostly verified but overstated:

  • βœ… Factory has 1,160 LOC with 22 hooks (not 23)
  • βœ… SSE wiring is fragile (confirmed)
  • ❌ 30-40% LOC reduction potential is OVERSTATED (actual: 5-6% to core)

Primary value is NOT LOC reduction but:

  1. Separation of concerns: Business logic β†’ testable core
  2. Reusability: TUI, CLI can use same logic
  3. Maintainability: Pure functions easier to reason about than closures

Recommendation:

  • βœ… Focus on HIGH PRIORITY moves first (slash commands, context calc, status bootstrap)
  • βœ… Accept that React layer WILL have significant LOC (it's UI binding code)
  • βœ… SSE wiring automation is the REAL fragility fix, not core extraction
  • ❌ Don't expect massive LOC reduction - the complexity is legitimate

Next Steps:

  1. Review this audit with team
  2. Prioritize Phase 1 extractions (slash commands, context calc, status bootstrap)
  3. Create implementation plan for Phase 2 (SSE wiring automation)
  4. Update ADR-015 with realistic expectations (5-6% reduction, not 30-40%)