-
Notifications
You must be signed in to change notification settings - Fork 5
🤖 Stream .cmux/init hook on workspace creation #228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
ammar-agent
wants to merge
17
commits into
main
Choose a base branch
from
feat/init-hook
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,064
−15
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
d5750bc to
d0170f2
Compare
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.
This reverts commit ba5cc88.
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
ba5cc88 to
97cde93
Compare
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
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Summary
Completes end-to-end "init hook streaming" infrastructure. The
.cmux/inithook 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)
workspace:meta:subscribe/unsubscribehandlers and malformed braces insrc/services/ipcMain.tsimport("@/types/workspace").WorkspaceMetaEventwith top-levelimport type { WorkspaceMetaEvent }to resolve eslint errorsvoid this.runWorkspaceInitHook()) - fire-and-forget with line-buffered streaming and non-interactive envmetaEventBufferstores events per workspace; late subscribers receive replay viaworkspace:meta:subscribePreload API
onMeta(workspaceId: string, callback: (data: WorkspaceMetaEvent) => void)with proper type importRenderer (UI)
WorkspaceMetaEvent(noany), preferslinein error events when present, elseerrorTypes (shared)
src/types/ipc.tsusesimport type { WorkspaceMetaEvent }instead of inline importTests
tests/ipcMain/workspaceInitHook.test.ts):.cmux/initabsentenv.sentEvents(not mockIpcRenderer.on) to detect meta eventsNon-goals (can be follow-ups)
Testing
make lint,make fmt,make typecheckall passTEST_INTEGRATION=1 bun x jest tests/ipcMain/workspaceInitHook.test.tsGenerated with
cmux