Skip to content

Commit 676e474

Browse files
committed
docs: Add init hooks documentation
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.
1 parent ce3b2de commit 676e474

File tree

5 files changed

+75
-3
lines changed

5 files changed

+75
-3
lines changed

docs/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
- [Workspaces](./workspaces.md)
1212
- [Forking](./fork.md)
13+
- [Init Hooks](./init-hooks.md)
1314
- [Models](./models.md)
1415
- [Keyboard Shortcuts](./keybinds.md)
1516
- [Vim Mode](./vim-mode.md)

docs/init-hooks.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Init Hooks
2+
3+
Add a `.cmux/init` executable script to your project root to run commands when creating new workspaces.
4+
5+
## Example
6+
7+
```bash
8+
#!/bin/bash
9+
set -e
10+
11+
bun install
12+
bun run build
13+
```
14+
15+
Make it executable:
16+
17+
```bash
18+
chmod +x .cmux/init
19+
```
20+
21+
## Behavior
22+
23+
- **Runs once** per workspace on creation
24+
- **Streams output** to the workspace UI in real-time
25+
- **Non-blocking** - workspace is immediately usable, even while hook runs
26+
- **Exit codes preserved** - failures are logged but don't prevent workspace usage
27+
28+
The init script runs in the workspace directory with the workspace's environment.
29+
30+
## Use Cases
31+
32+
- Install dependencies (`npm install`, `bun install`, etc.)
33+
- Run build steps
34+
- Generate code or configs
35+
- Set up databases or services
36+
- Warm caches
37+
38+
## Output
39+
40+
Init output appears in a banner at the top of the workspace. Click to expand/collapse the log. The banner shows:
41+
- Script path (`.cmux/init`)
42+
- Status (running, success, or exit code on failure)
43+
- Full stdout/stderr output
44+
45+
## Idempotency
46+
47+
The hook runs every time you create a workspace, even if you delete and recreate with the same name. Make your script idempotent if you're modifying shared state.
48+

src/hooks/useWorkspaceManagement.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,23 @@ export function useWorkspaceManagement({
9797
const loadedProjects = new Map<string, ProjectConfig>(projectsList);
9898
onProjectsUpdate(loadedProjects);
9999

100-
// Reload workspace metadata to get the new workspace ID
100+
// OPTIMIZATION: Subscribe immediately to workspace to receive init hook events in real-time
101+
// Without this, we'd wait for loadWorkspaceMetadata() + React effect, during which
102+
// early init hook events would be emitted but dropped (not subscribed yet).
103+
// Those events would then be replayed in a batch when subscription finally happens,
104+
// losing the real-time streaming UX.
105+
const { getWorkspaceStoreForEagerSubscription } = await import("@/stores/WorkspaceStore");
106+
const workspaceStore = getWorkspaceStoreForEagerSubscription();
107+
workspaceStore.addWorkspace({
108+
id: result.metadata.id,
109+
name: result.metadata.name,
110+
projectName: result.metadata.projectName,
111+
projectPath: result.metadata.projectPath,
112+
namedWorkspacePath: result.metadata.namedWorkspacePath,
113+
createdAt: result.metadata.createdAt,
114+
});
115+
116+
// Reload workspace metadata to get the new workspace ID (for consistency with other state)
101117
await loadWorkspaceMetadata();
102118

103119
// Return the new workspace selection

src/stores/WorkspaceStore.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,6 +1024,15 @@ function getStoreInstance(): WorkspaceStore {
10241024
return storeInstance;
10251025
}
10261026

1027+
/**
1028+
* Get the WorkspaceStore instance for eager workspace subscription.
1029+
* Used by useWorkspaceManagement to subscribe to new workspaces immediately
1030+
* after creation, before React effects run.
1031+
*/
1032+
export function getWorkspaceStoreForEagerSubscription(): WorkspaceStore {
1033+
return getStoreInstance();
1034+
}
1035+
10271036
/**
10281037
* Hook to get state for a specific workspace.
10291038
* Only re-renders when THIS workspace's state changes.

src/utils/eventStore.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,6 @@ export class EventStore<TState, TEvent> {
147147
state = diskState;
148148
}
149149

150-
log.debug(`[${this.storeName}] Replaying events for ${workspaceId}`);
151-
152150
// Augment state with context for serialization
153151
const augmentedState = { ...state, ...context };
154152

0 commit comments

Comments
 (0)