Skip to content

Conversation

@ammar-agent
Copy link
Collaborator

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

Summary

Streams .cmux/init hook output live to UI during workspace creation. The init hook is now fully functional with proper event streaming, race-condition-free replay, and clean UX.

Key Features

  • Live streaming: Init hook output appears in real-time as lines are emitted
  • Race-free: Backend ensures state exists before IPC returns, frontend subscribes and replays without buffering issues
  • Persistent: Init messages remain visible in UI (success and error states)
  • React-optimized: Proper array references trigger React.memo change detection
  • Clean UX: Init messages display at top of chat with status indicators

Implementation

Backend Changes

  • src/services/ipcMain.ts: Await startWorkspaceInitHook() to ensure state exists before workspace creation returns
  • src/services/initStateManager.ts: Manages init state lifecycle, disk persistence, event emission
  • src/services/AgentSession.ts: Forwards init events to IPC, replays init state before caught-up signal

Frontend Changes

  • src/stores/WorkspaceStore.ts: Processes init events immediately (not buffered), handles subscription/replay
  • src/utils/messages/StreamingMessageAggregator.ts: Converts init state to DisplayedMessage with shallow array copies for React
  • src/components/Messages/InitMessage.tsx: Renders init output with status indicators

Cleanup

  • Removed debug logging from init event handlers
  • Removed auto-dismiss timeout (init messages persist)
  • Simplified defensive checks (race condition fixed)
  • Added .cmux/init hook that runs bun install

Testing

  • 773 unit tests passing
  • 5 integration tests passing (init hook streaming with real timing verification)
  • Tests verify events arrive ~100ms apart (not batched), matching script sleep intervals
  • Replay behavior matches live streaming (invariant preserved)

Example

When creating a workspace, if .cmux/init exists:

#!/usr/bin/env bash
echo "Installing dependencies..."
bun install
echo "Done!"

Output streams live to UI:

🔄 Running init hook (.cmux/init)
Installing dependencies...
[bun install output...]
Done!
✅ Init completed successfully (exit code 0)

Generated with cmux

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

fix: Add onMeta implementation to browser API

refactor: Unify init hooks with chat stream

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.

🤖 refactor: Integrate init hooks into DisplayedMessage pattern and centralize 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

🤖 feat: Add comprehensive logging to init hook execution

- 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.

🤖 fix: Wire up init events to frontend WorkspaceStore

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.

Revert "🤖 fix: Wire up init events to frontend WorkspaceStore"

This reverts commit ba5cc88.

🤖 refactor: extract EventStore abstraction from InitStateManager

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.

- **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

- 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

- 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`_

🤖 fix: wire up init events to frontend WorkspaceStore

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`_

🤖 refactor: document caught-up optimization and simplify init event flow

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`_

🤖 docs: clarify init event buffering comment

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`_

🤖 feat: add init hook tip to workspace EmptyState

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`_

🤖 feat: display init hook script path in banner for debugging

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`_

fix: Process init events immediately for real-time display

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

refactor: Convert styling to Tailwind and fix caught-up timing

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

fix: Subscribe to workspace immediately after creation for real-time 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.
- Move replayInit() before caught-up event in agentSession.ts
  - Init events are historical data and should be replayed before caught-up signal
  - Ensures frontend buffers init events correctly with other historical data

- Remove eager subscription workaround
  - Removed getWorkspaceStoreForEagerSubscription() from WorkspaceStore
  - Removed eager subscription logic from useWorkspaceManagement
  - Workaround was unnecessary with correct replay order

- Improve TimedLine data structure
  - Replace lines: string[] with lines: TimedLine[] in InitStatus
  - TimedLine = { line, isError, timestamp }
  - Store isError as boolean instead of "ERROR:" prefix hack
  - Preserve timestamps for accurate event replay

- Fix test timing measurement
  - Capture timestamps when events are sent, not when observed
  - Update TestEnvironment to include timestamp in sentEvents
  - Fix workspaceInitHook test to use real event timestamps
  - Adjust first-event delay expectation (500ms → 1000ms for bash startup)

- Update all tests for new TimedLine structure
  - Fix initStateManager unit tests
  - Fix persistence integration test
  - All 769 unit tests + 5 integration tests passing

Events now stream with natural timing (~120-140ms apart) matching the
100ms sleep between lines in test hooks. The "batching" issue was a
test measurement bug - events were streaming correctly all along.
Root cause: React.memo change detection failing due to array reference reuse.

When init-output events arrived, StreamingMessageAggregator would:
1. Push new line to this.initState.lines array (mutating in place)
2. Invalidate cache and rebuild DisplayedMessage
3. BUT: new DisplayedMessage referenced the SAME lines array

React.memo saw identical array reference and skipped re-render, so UI
only updated when init-end arrived (status change forced re-render).

Fix: Create shallow copy of lines array when building DisplayedMessage.
Now each init-output creates new array reference, triggering re-render.

Also removed init events from isStreamEvent() - they're workspace
lifecycle events that should process immediately, not buffer until
caught-up like regular stream events.

All 5 integration tests passing with ~120ms streaming timing.
Add 4 unit tests for StreamingMessageAggregator to ensure proper
reference stability for React.memo change detection:

1. **Array reference changes on state change**: Verifies getDisplayedMessages()
   returns new array when init state changes (cache invalidation works)

2. **Lines array gets shallow copied**: Critical test - ensures lines array
   is a new reference when init-output arrives, not the same mutated array

3. **New init message object per change**: Verifies each state change creates
   new DisplayedMessage object with new lines array reference

4. **Cache returns same reference when unchanged**: Verifies optimization -
   repeated calls without state changes return cached reference

These tests would have caught the bug where lines array was directly
referenced instead of shallow copied, breaking React.memo detection.
Add console.debug logs to StreamingMessageAggregator for init events:

- init-start: Log hookPath and timestamp
- init-output: Log line content, isError flag, and running total
- init-end: Log exit code, status, and total lines
- Auto-dismiss: Log when successful init is auto-dismissed
- Serialization: Log when init state is converted to DisplayedMessage

Also add warnings when init-output/init-end arrive without active
init state (helps catch event ordering bugs).

These logs help debug:
- Init event timing and order
- State transitions (running → success/error)
- React render triggers (cache invalidation)
- Reference stability issues (can verify new array created)
Add console.debug logs for init-start, init-output, and init-end events:

- init-start: Log hookPath and timestamp when init begins
- init-output: Log line content, isError flag, and running line count
- init-end: Log exit code, final status, and total line count
- Auto-dismiss: Log when successful init is auto-dismissed after 800ms

Also add warnings when init-output/init-end arrive without active
init state (helps catch event ordering bugs).

These logs complement the existing serialization log, providing full
visibility into init event lifecycle from arrival through rendering.
Root cause: workspace.create returned before init hook started, causing
race between event emission and frontend subscription. Early events were
lost (emitted before IPC listener registered).

Solution: Refactor runWorkspaceInitHook → startWorkspaceInitHook (async)
and await it in workspace.create. Now:

1. Create workspace metadata
2. Call startInit() to create in-memory state
3. Return from workspace.create (frontend can now subscribe)
4. Init hook process runs async (emits events to subscribed frontend)

This guarantees:
- In-memory state exists before workspace.create returns
- replayInit() always finds state (no empty replay)
- All init events have active subscription (none lost)
- Fast: only waits for hook to START (~instant), not complete

Live streaming and replay now produce identical UI states.

Net change: ~10 LoC (refactor fire-and-forget to async/await)
@ammario ammario marked this pull request as ready for review October 24, 2025 16:02
- Remove console.debug/log statements from StreamingMessageAggregator
  - Init event logging (start, output, end, auto-dismiss)
  - Tool call logging (start, delta)
- Remove auto-dismiss timeout for successful init messages
  - Init messages now persist in UI regardless of status
- Remove defensive null checks in init event handling
  - Race condition fixed in previous commit guarantees state exists
- Simplify comment in WorkspaceStore for init event processing
- Remove debug console.log from integration test timing measurements
- Add .cmux/init hook that runs bun install for new workspaces

Generated with `cmux`
- Remove unnecessary async from IIFE in ipcMain.ts (no await inside)
- Add type assertions for expect.any(Number) in tests to fix unsafe assignment warnings

Generated with `cmux`
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Fixes issue where init hooks fail silently when project paths contain
spaces (e.g., '~/Code/My Project/.cmux/init'). The bash service runs
commands with 'bash -c', which requires proper quoting.

Generated with `cmux`
Init events already bypass buffering because:
- They have a 'type' field, so don't match the historical message check
- The regular message path handles them correctly
- This removes 5 LoC and simplifies the logic

Generated with `cmux`
After removing the special case, init events now flow through the
regular message path. Updated comment to clarify what's excluded
from stream event buffering.

Generated with `cmux`
Init events ARE persisted to disk (init-status.json), just not to
chat history (chat.jsonl). Updated comment to reflect this.

Generated with `cmux`
The cleanup removed necessary defensive checks, causing crashes when
init-output or init-end arrive without init-start (can happen during
replay or out-of-order events). Restored graceful handling.

Added TDD test to prevent regression.

Generated with `cmux`
After removing special case, init events fell through the buffering check
but never reached processing logic. Changed to process all non-bufferable
events immediately (init events, live stream events, etc.)

This was the actual bug preventing init display in UI.

Generated with `cmux`
@ammar-agent ammar-agent force-pushed the feat/init-hook branch 2 times, most recently from afe83cd to 20bdcc6 Compare October 24, 2025 16:41
Init events can arrive before or after caught-up:
- Before: During replay from init-status.json (historical)
- After: During live workspace creation (real-time)

Like stream events, init events are now explicitly buffered during
replay to avoid O(N) re-renders and ensure proper ordering.

Added detailed comments explaining this behavior.

Generated with `cmux`
Init events are buffered like stream events but were missing handlers
in processStreamEvent(), causing them to be silently dropped.

Generated with `cmux`
Replaced separate isStreamEvent() check and processStreamEvent() dispatch
with a single event handler map. This makes it impossible to add a buffered
event type without a handler:

- Single source of truth: bufferedEventHandlers map defines both which
  events to buffer (keys) and how to handle them (values)
- No synchronization bugs: Can't add to one without the other
- Simpler code: ~100 LoC of if-chains replaced with map dispatch
- Self-documenting: Map keys show all buffered event types at a glance

Net: -30 LoC, same behavior, zero risk of silent drops

Generated with `cmux`
@ammario ammario merged commit c596556 into main Oct 24, 2025
15 checks passed
@ammario ammario deleted the feat/init-hook branch October 24, 2025 17:17
ammar-agent added a commit that referenced this pull request Oct 24, 2025
Fixes two issues found after PR #228:

1. **Init-output undefined line crash**: Added null check for data.line
   before calling trimEnd(). Prevents crash when init-output events
   arrive with missing line data during replay or out-of-order scenarios.

2. **Message processing condition tightened**: Added 'role' field check
   to ensure only CmuxMessages are processed in the caught-up branch.
   This makes the condition symmetric with the buffering branch and
   ensures we don't accidentally process malformed messages.

Both changes follow the defensive programming pattern established in
PR #228 - gracefully handle edge cases during replay rather than crash.

_Generated with `cmux`_
ammario pushed a commit that referenced this pull request Oct 24, 2025
## Problem

After #228 merged, encountered two issues:

1. **Init-output crash**: `StreamingMessageAggregator.ts:516` throws
`Cannot read properties of undefined (reading 'trimEnd')` when
processing init-output events with missing `line` field
2. **Inconsistent message appearance**: User messages sometimes don't
appear immediately in chat view

## Solution

### 1. Init-output defensive check
Added null check for `data.line` before calling `trimEnd()`:
```typescript
if (!data.line) return; // Defensive: skip events with missing line data
```

Follows the defensive pattern from #228 - gracefully handle edge cases
during replay.

### 2. Message processing condition tightened  
Added `'role' in data` check to the caught-up branch:
```typescript
} else if (isCaughtUp && "role" in data) {
```

Makes the condition symmetric with the buffering branch and ensures only
valid CmuxMessages are processed.

## Testing

- ✅ `make typecheck` passes
- ✅ All 763 unit tests pass

_Generated with `cmux`_
ammar-agent added a commit that referenced this pull request Oct 24, 2025
Connects the init hooks system (PR #228) with the Runtime abstraction so
workspace creation progress and init hook output stream to the frontend.

**Init Hook Utilities (src/runtime/initHook.ts):**

- checkInitHookExists(): Check if .cmux/init is executable
- getInitHookPath(): Get init hook path for project
- LineBuffer class: Line-buffered streaming (handles incomplete lines)
- createLineBufferedLoggers(): Creates stdout/stderr line buffers

**Runtime Integration:**

- InitLogger interface: logStep(), logStdout(), logStderr(), logComplete()
- WorkspaceCreationParams extended with initLogger
- LocalRuntime: Runs init hook locally via bash, streams output
- SSHRuntime: Runs init hook on remote host, streams via Web Streams

**IPC Bridge:**

- IpcMain creates InitLogger that bridges to InitStateManager
- Runtime owns workspace creation entirely (no IPC branching)
- Creation steps logged: "Creating worktree...", "Running init hook..."
- Real-time streaming to frontend via existing init channels

**Testing:**

- 7 unit tests for LineBuffer and createLineBufferedLoggers
- Integration tests updated with mockInitLogger
- All 770 tests passing

Generated with `cmux`
ammar-agent added a commit that referenced this pull request Oct 24, 2025
Connects the init hooks system (PR #228) with the Runtime abstraction so
workspace creation progress and init hook output stream to the frontend.

**Init Hook Utilities (src/runtime/initHook.ts):**

- checkInitHookExists(): Check if .cmux/init is executable
- getInitHookPath(): Get init hook path for project
- LineBuffer class: Line-buffered streaming (handles incomplete lines)
- createLineBufferedLoggers(): Creates stdout/stderr line buffers

**Runtime Integration:**

- InitLogger interface: logStep(), logStdout(), logStderr(), logComplete()
- WorkspaceCreationParams extended with initLogger
- LocalRuntime: Runs init hook locally via bash, streams output
- SSHRuntime: Runs init hook on remote host, streams via Web Streams

**IPC Bridge:**

- IpcMain creates InitLogger that bridges to InitStateManager
- Runtime owns workspace creation entirely (no IPC branching)
- Creation steps logged: "Creating worktree...", "Running init hook..."
- Real-time streaming to frontend via existing init channels

**Testing:**

- 7 unit tests for LineBuffer and createLineBufferedLoggers
- Integration tests updated with mockInitLogger
- All 770 tests passing

Generated with `cmux`
ammar-agent added a commit that referenced this pull request Oct 25, 2025
Connects the init hooks system (PR #228) with the Runtime abstraction so
workspace creation progress and init hook output stream to the frontend.

**Init Hook Utilities (src/runtime/initHook.ts):**

- checkInitHookExists(): Check if .cmux/init is executable
- getInitHookPath(): Get init hook path for project
- LineBuffer class: Line-buffered streaming (handles incomplete lines)
- createLineBufferedLoggers(): Creates stdout/stderr line buffers

**Runtime Integration:**

- InitLogger interface: logStep(), logStdout(), logStderr(), logComplete()
- WorkspaceCreationParams extended with initLogger
- LocalRuntime: Runs init hook locally via bash, streams output
- SSHRuntime: Runs init hook on remote host, streams via Web Streams

**IPC Bridge:**

- IpcMain creates InitLogger that bridges to InitStateManager
- Runtime owns workspace creation entirely (no IPC branching)
- Creation steps logged: "Creating worktree...", "Running init hook..."
- Real-time streaming to frontend via existing init channels

**Testing:**

- 7 unit tests for LineBuffer and createLineBufferedLoggers
- Integration tests updated with mockInitLogger
- All 770 tests passing

Generated with `cmux`
ammar-agent added a commit that referenced this pull request Oct 25, 2025
Connects the init hooks system (PR #228) with the Runtime abstraction so
workspace creation progress and init hook output stream to the frontend.

**Init Hook Utilities (src/runtime/initHook.ts):**

- checkInitHookExists(): Check if .cmux/init is executable
- getInitHookPath(): Get init hook path for project
- LineBuffer class: Line-buffered streaming (handles incomplete lines)
- createLineBufferedLoggers(): Creates stdout/stderr line buffers

**Runtime Integration:**

- InitLogger interface: logStep(), logStdout(), logStderr(), logComplete()
- WorkspaceCreationParams extended with initLogger
- LocalRuntime: Runs init hook locally via bash, streams output
- SSHRuntime: Runs init hook on remote host, streams via Web Streams

**IPC Bridge:**

- IpcMain creates InitLogger that bridges to InitStateManager
- Runtime owns workspace creation entirely (no IPC branching)
- Creation steps logged: "Creating worktree...", "Running init hook..."
- Real-time streaming to frontend via existing init channels

**Testing:**

- 7 unit tests for LineBuffer and createLineBufferedLoggers
- Integration tests updated with mockInitLogger
- All 770 tests passing

Generated with `cmux`
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.

2 participants