Cell: opencode-next--xts0a-mjuyf47sofd
Epic: Event Architecture Simplification
Date: 2025-12-31
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)
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())
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))
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
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
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)
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 | nullBenefits:
- Clear business logic extraction
- Store becomes:
const c = detectCompactionStart(msg); if (c) dir.compaction[id] = c
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
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
Pure React Patterns:
use-live-time.ts- Timer-based re-renders (React-specific)use-sse.ts- EventSource lifecycle tied to useEffectuse-multi-server-sse.ts- React subscription to multiServerSSE singletonuse-subagent.ts- Zustand selector + local expand stateuse-session-data.ts- RSC hydration + Zustand selectorsuse-messages-with-parts.ts- useMemo joining (React optimization)
Why:
- Component lifecycle (mount/unmount) management
- SSR safety (useEffect guards)
- React-specific optimizations (useMemo, useCallback)
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:
- Developer forgets
useSSEEvents()β silent data staleness - Multiple calls β multiple subscriptions (safe but wasteful)
- No compile-time safety
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:
useSSEEventsIS 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
useSSEEventsnot found in layout - Add runtime warning if store receives events but
useSSEEventsnever called
HIGH PRIORITY (~217 LOC to core):
-
Slash Command Parsing (40 LOC)
- From:
factory.tslines 262-300 - To:
@opencode-vibe/core/api/commands.ts - Benefit: Reusable in TUI, CLI, testable without React
- From:
-
Context Usage Calculation (27 LOC)
- From:
store.tslines 367-393 - To:
@opencode-vibe/core/api/context.ts - Benefit: Pure function, testable, reusable
- From:
-
Session Status Bootstrap (150 LOC)
- From:
use-multi-directory-status.tslines 100-151 - To:
@opencode-vibe/core/api/status.ts - Benefit: Largest win, removes complex async logic from hook
- From:
Implementation steps:
- Create new core API modules with extracted functions
- Add tests for core functions (easier without React)
- Update React layer to call new core functions
- Verify all existing tests pass
- 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
Move useSSEEvents() to root layout:
- Add
useSSEEvents()call inapps/web/src/app/layout.tsx - Remove per-page calls (document/layout.tsx, session/[id]/layout.tsx, etc.)
- Add runtime warning if SSE events received but hook never called
- Document pattern in factory JSDoc
Benefits:
- Eliminates fragile per-page wiring
- Prevents silent failures
- Single source of truth for SSE initialization
MEDIUM PRIORITY (~190 LOC to core):
-
Message Queue Logic (130 LOC)
- From:
factory.tslines 302-434 - To:
@opencode-vibe/core/api/sessions.ts - Concern: Needs session status integration design
- From:
-
Compaction Detection (30 LOC)
- From:
store.tslines 395-404, 442-464 - To:
@opencode-vibe/core/api/compaction.ts - Benefit: Clear business logic extraction
- From:
-
Fuzzy File Search (30 LOC)
- From:
factory.tslines 637-667 - To:
@opencode-vibe/core/api/find.ts - Benefit: Small but reusable
- From:
| 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)
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:
- Separation of concerns: Business logic β testable core
- Reusability: TUI, CLI can use same logic
- 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:
- Review this audit with team
- Prioritize Phase 1 extractions (slash commands, context calc, status bootstrap)
- Create implementation plan for Phase 2 (SSE wiring automation)
- Update ADR-015 with realistic expectations (5-6% reduction, not 30-40%)