Skip to content

Conversation

@ammar-agent
Copy link
Collaborator

@ammar-agent ammar-agent commented Oct 13, 2025

Summary

Completes end-to-end "init hook streaming" infrastructure. The .cmux/init hook is now detected, executed, and its output streamed live to the UI on workspace creation. Non-zero exit codes are allowed; workspace remains usable regardless.

Changes

Backend (Electron main)

  • Remove duplicate handlers: Cleaned up 3 copies of workspace:meta:subscribe/unsubscribe handlers and malformed braces in src/services/ipcMain.ts
  • Fix type imports: Replaced inline import("@/types/workspace").WorkspaceMetaEvent with top-level import type { WorkspaceMetaEvent } to resolve eslint errors
  • Hook invocation: Already present at workspace creation (void this.runWorkspaceInitHook()) - fire-and-forget with line-buffered streaming and non-interactive env
  • Buffer and replay: metaEventBuffer stores events per workspace; late subscribers receive replay via workspace:meta:subscribe

Preload API

  • Strong typing: onMeta(workspaceId: string, callback: (data: WorkspaceMetaEvent) => void) with proper type import

Renderer (UI)

  • Typed event handling: AIView.tsx uses WorkspaceMetaEvent (no any), prefers line in error events when present, else error
  • Success auto-hide, failure persistent: Banner disappears on exit code 0, stays visible on non-zero

Types (shared)

  • Import cleanup: src/types/ipc.ts uses import type { WorkspaceMetaEvent } instead of inline import

Tests

  • Integration suite (tests/ipcMain/workspaceInitHook.test.ts):
    • ✅ Hook success: verifies start → output/error → end(0) sequence
    • ✅ Hook failure: verifies workspace remains usable on exit code 1
    • ✅ No hook: verifies no events emitted when .cmux/init absent
    • Tests poll env.sentEvents (not mockIpcRenderer.on) to detect meta events

Non-goals (can be follow-ups)

  • Memory cap for meta buffer (e.g., last N events)
  • Windows fallback (PowerShell/cmd if bash unavailable)
  • Passing project secrets to hook env

Testing

  • make lint, make fmt, make typecheck all pass
  • Integration tests pass: TEST_INTEGRATION=1 bun x jest tests/ipcMain/workspaceInitHook.test.ts
  • App builds without errors

Generated with cmux

@ammar-agent ammar-agent force-pushed the feat/init-hook branch 4 times, most recently from d5750bc to d0170f2 Compare October 21, 2025 01:04
Complete end-to-end infrastructure for streaming .cmux/init hook output:
- Backend detects and runs optional project-level .cmux/init on workspace creation
- Stream stdout/stderr lines to renderer via WORKSPACE_STREAM_META IPC channel
- Buffer output for late subscribers; replay on subscribe
- UI displays live output with auto-hide on success, persistent banner on failure
- Workspace remains usable regardless of hook exit code

Backend (ipcMain.ts):
- Add WorkspaceMetaEvent type imports (fix inline import() eslint errors)
- Implement runWorkspaceInitHook with line-buffered streaming
- Add metaEventBuffer for replay to late subscribers
- Remove duplicate workspace:meta:subscribe handlers

Frontend (AIView.tsx):
- Add typed WorkspaceMetaEvent handler (no any types)
- Prefer line when present in error events, else error field
- Auto-hide success banner (800ms), persist failure banner

Types:
- Add WorkspaceMetaEvent union (start/output/error/end)
- Strong typing throughout preload and IPC layer

Tests:
- Add integration test suite (workspaceInitHook.test.ts)
- Verify start/output/error/end event sequence
- Verify workspace remains usable on hook failure
- Verify no events when hook absent

Generated with cmux
Simplifies init hook architecture by reusing existing chat stream
infrastructure instead of creating parallel IPC system.

Changes:
- Add WorkspaceInitEvent union to WorkspaceChatMessage (3 event types)
- Backend emits init events via AgentSession.emitChatEvent()
- Frontend handles init events from chat subscription
- Tests filter init events from chat channel
- Remove metaEventBuffer, onMeta(), and WORKSPACE_STREAM_META

Benefits:
- ~80 net LoC reduction (removed ~150, added ~70)
- 1 subscription per workspace instead of 2
- Automatic replay via existing history mechanism
- Cleaner types (no redundant workspaceId field)
- Single buffer for all workspace events

Init events are ephemeral (not persisted) and flow through the same
stream as caught-up, stream-error, and delete messages.
…ntralize bash execution

- Added workspace-init DisplayedMessage type with status tracking
- Extended StreamingMessageAggregator to convert init events to DisplayedMessage
- Created InitMessage component to render init banners in message stream
- Removed local init state management from AIView (eliminated parallel infrastructure)
- Removed legacy WorkspaceMetaEvent type (no longer used)

- Created BashExecutionService to centralize all bash execution
- Provides single abstraction point for future host migration (containers, remote, etc.)
- Eliminates duplicate environment setup across init hooks and bash tool
- executeStreaming() mode for line-by-line output (init hooks)
- Updated IpcMain to use BashExecutionService for init hook execution

Benefits:
- Init events flow through same path as other workspace events
- Centralized state management (no local component state)
- Single source of truth for bash environment setup
- Easier to abstract workspace hosts in future

Tests:
- Added unit tests for aggregator init handling (2 tests)
- All integration tests passing (3/3 init hook tests)
- Typecheck passing for both renderer and main processes
- Added log.debug() for init hook detection and script execution
- Added log.info() for init hook start and completion with exit codes
- Added log.error() for init hook failures
- Added logging to BashExecutionService for streaming command execution
- Added process error logging for bash execution failures

This improves debuggability when init hooks don't work as expected.
Init events were being buffered until "caught-up" but init hooks run during
workspace creation, before the workspace has any history or caught-up status.

Changes:
- Added isInitStart, isInitOutput, isInitEnd imports to WorkspaceStore
- Updated isStreamEvent() to include init events (process immediately)
- Added explicit init event handling in processStreamEvent()
- Init events now bypass caught-up check and process immediately

This ensures the init banner appears in the UI when workspaces are created.
Abstracts the shared event storage and replay pattern into a reusable
EventStore utility, eliminating duplication within InitStateManager and
establishing a pattern for future StreamManager refactoring.

## Key Changes

- **EventStore<TState, TEvent>** (199 LoC): Generic utility for managing
  workspace state with in-memory Map, disk persistence, and replay logic
- **InitStateManager refactored**: Simplified from 252→246 LoC using EventStore,
  eliminating duplicated event emission and manual replay loops
- **serializeInitEvents()**: Single method generates all replay events from state

## Benefits

- Eliminates internal duplication (repeated event emission, manual replay loops)
- Composition-based design (inject emit function, no EventEmitter inheritance)
- Type-safe with generic TState/TEvent parameters
- Context injection for replay (e.g., workspaceId augmentation)
- Documented pattern for future StreamManager adoption

## Testing

- EventStore: 16 unit tests (state management, persistence, replay, integration)
- InitStateManager: All 12 tests pass unchanged
- Integration: All 4 init hook tests pass
- Total: 748/748 tests passing (no regressions)

_Generated with `cmux`_
Init events were being buffered until "caught-up" but init hooks run during
workspace creation, before the workspace has any history or caught-up status.

Changes:
- Added isInitStart, isInitOutput, isInitEnd imports to WorkspaceStore
- Updated isStreamEvent() to include init events (process immediately)
- Added explicit init event handling in processStreamEvent()
- Init events now bypass caught-up check and process immediately

This ensures the init banner appears in the UI when workspaces are created.

Backend integration tests verify that init events are correctly emitted
through IPC to the frontend.

_Generated with `cmux`_
Added comprehensive documentation explaining why WorkspaceStore buffers events
until "caught-up" status - to avoid O(N) re-renders when loading workspaces
with long histories.

Changes:
- Added detailed comment block explaining caught-up buffering optimization
- Clarified that init events are buffered like other stream events
- Updated comment in processStreamEvent() to reflect buffered init events
- Init events now follow the same flow as other stream events (buffered until caught-up)

This simplifies the code by removing special-case handling - init events are
just another type of stream event that benefits from the buffering optimization.

_Generated with `cmux`_
The previous comment at line 959 was misleading - it said 'processed after
caught-up' but appeared to be before any caught-up check.

Clarified that:
- Init events ARE buffered in handleChatMessage() until caught-up
- The code at line 959 processes them AFTER buffering/replay
- By the time we reach this code, buffering decision already happened

This makes the control flow clearer: handleChatMessage() does buffering,
processStreamEvent() does processing.

_Generated with `cmux`_
Updated the "No Messages Yet" empty state in workspace view to educate
users about the .cmux/init hook feature.

Changes:
- Added tip about .cmux/init hook below the main empty state message
- Styled code tag with monospace font, subtle background, and color
- Tip appears in smaller, muted text with lightbulb emoji
- Briefly explains use case (install dependencies, build)

This provides discoverable education for new users who create workspaces
and helps them understand how to set up automated initialization.

_Generated with `cmux`_
The init hook banner now shows the full path to the script being executed,
making it easier for users to debug initialization issues.

Changes:
- Added `hookPath` field to workspace-init DisplayedMessage type
- Updated StreamingMessageAggregator to capture and track hookPath from init-start events
- Enhanced InitMessage component to display hookPath below status message
- Styled hookPath with muted monospace font for clarity

The hookPath appears in smaller, muted text below the main status message,
helping users quickly identify which script is running and where to find it.

Output streaming already worked - init-output events were already being
accumulated and displayed in the InitHookLog component.

_Generated with `cmux`_
Init events were being buffered until 'caught-up', preventing real-time
display during workspace creation. Since init hooks run BEFORE any chat
history exists, they should be processed immediately, not buffered.

Changes:
- Init events now bypass the buffering logic in handleChatMessage()
- Updated comments to reflect that init events are not buffered
- All 764 unit tests pass
- All 4 init hook integration tests pass
Three improvements addressing code review feedback:

1. **AIView.tsx empty state** - Convert inline styles to Tailwind classes
   - Changed inline style object to `mt-5 text-xs text-[#888]`

2. **InitMessage.tsx** - Migrate from styled-components to Tailwind
   - Removed @emotion/styled dependency usage
   - Converted all styled components to Tailwind utility classes
   - Maintains identical visual appearance

3. **Fix caught-up timing for real-time init display**
   - Send caught-up IMMEDIATELY after chat history loads
   - Replay init events AFTER caught-up (not before)
   - Init events are workspace lifecycle metadata, not chat history
   - Eliminates O(N) re-renders during init hook execution

**Why this works:**
- Chat history = buffered until caught-up (prevents render thrashing)
- Init events = processed in real-time after caught-up (no buffering)
- New workspaces: caught-up sent instantly → init streams in real-time ✅
- Page reload: caught-up sent after history → init replayed from disk ✅

All 769 unit tests + 4 init hook integration tests pass.
Added user-facing documentation for .cmux/init hooks under Workspaces section.

Covers:
- Basic example with setup instructions
- Behavior (runs once, streams output, non-blocking, exit codes)
- Common use cases (deps, builds, codegen, services)
- Output display (banner with status and logs)
- Idempotency considerations

Follows docs/STYLE.md guidelines:
- Assumes technical competence
- Focuses on non-obvious behavior (non-blocking, idempotency)
- Concise and practical examples
- No obvious details
…init events

When a workspace is created, the init hook starts running immediately in
the background. However, the frontend previously waited for React effects
to process the workspace metadata update before subscribing to events.

This created a race condition where early init hook output lines were
emitted before the frontend subscribed, causing them to be dropped at the
WebSocket layer (only subscribed clients receive messages).

Although these events would be replayed when subscription finally happened,
this broke the real-time streaming UX - users saw all output appear at once
in a batch instead of streaming line-by-line.

Fix by calling workspaceStore.addWorkspace() immediately after receiving
the workspace creation response, before React effects run. This ensures
the frontend is subscribed before (or very quickly after) the init hook
starts emitting events, preserving the real-time streaming experience.

Also export getWorkspaceStoreForEagerSubscription() to allow non-React
code to access the singleton store instance for imperative operations.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant