Skip to content

Commit 97cde93

Browse files
committed
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
1 parent bd0378b commit 97cde93

File tree

2 files changed

+31
-209
lines changed

2 files changed

+31
-209
lines changed

src/components/AIView.tsx

Lines changed: 19 additions & 203 deletions
Original file line numberDiff line numberDiff line change
@@ -30,203 +30,6 @@ import { TooltipWrapper, Tooltip } from "./Tooltip";
3030
import type { DisplayedMessage } from "@/types/message";
3131
import { useAIViewKeybinds } from "@/hooks/useAIViewKeybinds";
3232

33-
const ViewContainer = styled.div`
34-
flex: 1;
35-
display: flex;
36-
flex-direction: row;
37-
background: #1e1e1e;
38-
color: #d4d4d4;
39-
font-family: var(--font-monospace);
40-
font-size: 12px;
41-
overflow-x: auto;
42-
overflow-y: hidden;
43-
container-type: inline-size;
44-
45-
/* Mobile: Stack vertically */
46-
@media (max-width: 768px) {
47-
flex-direction: column;
48-
}
49-
`;
50-
51-
const ChatArea = styled.div`
52-
flex: 1;
53-
min-width: 400px; /* Reduced from 750px to allow narrower layout when Review panel is wide */
54-
display: flex;
55-
flex-direction: column;
56-
57-
/* Mobile: Remove min-width and take full width */
58-
@media (max-width: 768px) {
59-
min-width: 0;
60-
width: 100%;
61-
max-height: 100%;
62-
}
63-
`;
64-
65-
const ViewHeader = styled.div`
66-
padding: 4px 15px;
67-
background: #252526;
68-
border-bottom: 1px solid #3e3e42;
69-
display: flex;
70-
justify-content: space-between;
71-
align-items: center;
72-
73-
/* Mobile: Add padding for hamburger button and adjust spacing */
74-
@media (max-width: 768px) {
75-
padding: 8px 15px 8px 60px; /* Extra left padding for hamburger button */
76-
flex-wrap: wrap;
77-
gap: 8px;
78-
}
79-
`;
80-
81-
const WorkspaceTitle = styled.div`
82-
font-weight: 600;
83-
color: #cccccc;
84-
display: flex;
85-
align-items: center;
86-
gap: 8px;
87-
min-width: 0; /* Allow flex children to shrink */
88-
overflow: hidden;
89-
`;
90-
91-
const WorkspacePath = styled.span`
92-
font-family: var(--font-monospace);
93-
color: #888;
94-
font-weight: 400;
95-
font-size: 11px;
96-
white-space: nowrap;
97-
overflow: hidden;
98-
text-overflow: ellipsis;
99-
min-width: 0;
100-
101-
/* Hide path on mobile to save space */
102-
@media (max-width: 768px) {
103-
display: none;
104-
}
105-
`;
106-
107-
const WorkspaceName = styled.span`
108-
white-space: nowrap;
109-
overflow: hidden;
110-
text-overflow: ellipsis;
111-
min-width: 0;
112-
`;
113-
114-
const TerminalIconButton = styled.button`
115-
background: transparent;
116-
border: none;
117-
cursor: pointer;
118-
padding: 4px;
119-
display: flex;
120-
align-items: center;
121-
justify-content: center;
122-
color: #888;
123-
transition: color 0.2s;
124-
125-
&:hover {
126-
color: #ccc;
127-
}
128-
129-
svg {
130-
width: 16px;
131-
height: 16px;
132-
}
133-
`;
134-
135-
const OutputContainer = styled.div`
136-
flex: 1;
137-
position: relative;
138-
overflow: hidden;
139-
`;
140-
141-
const OutputContent = styled.div`
142-
height: 100%;
143-
overflow-y: auto;
144-
padding: 15px;
145-
white-space: pre-wrap;
146-
word-break: break-word;
147-
line-height: 1.5;
148-
`;
149-
150-
const EmptyState = styled.div`
151-
flex: 1;
152-
display: flex;
153-
flex-direction: column;
154-
align-items: center;
155-
justify-content: center;
156-
height: 100%;
157-
color: #6b6b6b;
158-
text-align: center;
159-
160-
h3 {
161-
margin: 0 0 10px 0;
162-
font-size: 16px;
163-
font-weight: 500;
164-
}
165-
166-
p {
167-
margin: 0;
168-
font-size: 13px;
169-
}
170-
171-
code {
172-
font-family: var(--font-monospace);
173-
background: #2d2d30;
174-
padding: 2px 6px;
175-
border-radius: 3px;
176-
color: #d7ba7d;
177-
font-size: 11px;
178-
}
179-
`;
180-
181-
const EditBarrier = styled.div`
182-
margin: 20px 0;
183-
padding: 12px 15px;
184-
background: var(--color-editing-mode-alpha);
185-
border-bottom: 3px solid;
186-
border-image: repeating-linear-gradient(
187-
45deg,
188-
var(--color-editing-mode),
189-
var(--color-editing-mode) 10px,
190-
transparent 10px,
191-
transparent 20px
192-
)
193-
1;
194-
color: var(--color-editing-mode);
195-
font-size: 12px;
196-
font-weight: 500;
197-
text-align: center;
198-
`;
199-
200-
const JumpToBottomIndicator = styled.button`
201-
position: absolute;
202-
bottom: 8px;
203-
left: 50%;
204-
transform: translateX(-50%);
205-
padding: 4px 8px;
206-
background: hsl(from var(--color-assistant-border) h s l / 0.1);
207-
color: white;
208-
border: 1px solid hsl(from var(--color-assistant-border) h s l / 0.4);
209-
border-radius: 20px;
210-
font-size: 12px;
211-
font-weight: 500;
212-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
213-
cursor: pointer;
214-
transition: all 0.2s ease;
215-
z-index: 100;
216-
font-family: var(--font-primary);
217-
backdrop-filter: blur(1px);
218-
219-
&:hover {
220-
background: hsl(from var(--color-assistant-border) h s l / 0.4);
221-
border-color: hsl(from var(--color-assistant-border) h s l / 0.6);
222-
transform: translateX(-50%) scale(1.05);
223-
}
224-
225-
&:active {
226-
transform: translateX(-50%) scale(0.95);
227-
}
228-
`;
229-
23033
interface AIViewProps {
23134
workspaceId: string;
23235
projectName: string;
@@ -519,10 +322,19 @@ const AIViewInner: React.FC<AIViewProps> = ({
519322
}
520323

521324
return (
522-
<ViewContainer className={className}>
523-
<ChatArea ref={chatAreaRef}>
524-
<ViewHeader>
525-
<WorkspaceTitle>
325+
<div
326+
className={cn(
327+
"flex flex-1 flex-row bg-dark text-light overflow-x-auto overflow-y-hidden [@media(max-width:768px)]:flex-col",
328+
className
329+
)}
330+
style={{ containerType: "inline-size" }}
331+
>
332+
<div
333+
ref={chatAreaRef}
334+
className="flex min-w-96 flex-1 flex-col [@media(max-width:768px)]:max-h-full [@media(max-width:768px)]:w-full [@media(max-width:768px)]:min-w-0"
335+
>
336+
<div className="bg-separator border-border-light flex items-center justify-between border-b px-[15px] py-1 [@media(max-width:768px)]:flex-wrap [@media(max-width:768px)]:gap-2 [@media(max-width:768px)]:py-2 [@media(max-width:768px)]:pl-[60px]">
337+
<div className="text-foreground flex min-w-0 items-center gap-2 overflow-hidden font-semibold">
526338
<StatusIndicator
527339
streaming={canInterrupt}
528340
title={
@@ -574,11 +386,15 @@ const AIViewInner: React.FC<AIViewProps> = ({
574386
<h3>No Messages Yet</h3>
575387
<p>Send a message below to begin</p>
576388
<p style={{ marginTop: "20px", fontSize: "12px", color: "#888" }}>
577-
💡 Tip: Add a <code>.cmux/init</code> hook to your project to run setup commands
389+
💡 Tip: Add a{" "}
390+
<code className="rounded-[3px] bg-[#2d2d30] px-1.5 py-0.5 font-mono text-[11px] text-[#d7ba7d]">
391+
.cmux/init
392+
</code>{" "}
393+
hook to your project to run setup commands
578394
<br />
579395
(e.g., install dependencies, build) when creating new workspaces
580396
</p>
581-
</EmptyState>
397+
</div>
582398
) : (
583399
<>
584400
{mergedMessages.map((msg) => {

src/stores/WorkspaceStore.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -816,7 +816,6 @@ export class WorkspaceStore {
816816
this.replayingHistory.add(workspaceId);
817817

818818
// Process buffered stream events now that history is loaded
819-
// This includes init events that occurred during workspace creation
820819
const pendingEvents = this.pendingStreamEvents.get(workspaceId) ?? [];
821820
for (const event of pendingEvents) {
822821
this.processStreamEvent(workspaceId, aggregator, event);
@@ -842,17 +841,25 @@ export class WorkspaceStore {
842841
return;
843842
}
844843

844+
// Init events are processed immediately (not buffered)
845+
// They arrive BEFORE caught-up during workspace creation and represent
846+
// the initialization state of the workspace, not chat streaming state.
847+
// Buffering them would prevent real-time display during workspace creation.
848+
if (isInitStart(data) || isInitOutput(data) || isInitEnd(data)) {
849+
this.processStreamEvent(workspaceId, aggregator, data);
850+
return;
851+
}
852+
845853
// OPTIMIZATION: Buffer stream events until caught-up to reduce excess re-renders
846854
// When first subscribing to a workspace, we receive:
847855
// 1. Historical messages from chat.jsonl (potentially hundreds of messages)
848856
// 2. Partial stream state (if stream was interrupted)
849857
// 3. Active stream events (if currently streaming)
850-
// 4. Init events (if .cmux/init hook ran during workspace creation)
851858
//
852859
// Without buffering, each event would trigger a separate re-render as messages
853860
// arrive one-by-one over IPC. By buffering until "caught-up", we:
854861
// - Load all historical messages in one batch (O(1) render instead of O(N))
855-
// - Replay buffered stream/init events after history is loaded
862+
// - Replay buffered stream events after history is loaded
856863
// - Provide correct context for stream continuation (history is complete)
857864
//
858865
// This is especially important for workspaces with long histories (100+ messages),
@@ -985,9 +992,8 @@ export class WorkspaceStore {
985992
}
986993

987994
// Handle init events
988-
// Note: Init events are buffered in handleChatMessage() until caught-up,
989-
// then replayed here along with other buffered stream events.
990-
// This ensures they render with full historical context.
995+
// Note: Init events are processed immediately in handleChatMessage() (not buffered)
996+
// because they arrive during workspace creation before any chat history exists.
991997
if (isInitStart(data) || isInitOutput(data) || isInitEnd(data)) {
992998
aggregator.handleMessage(data);
993999
this.states.bump(workspaceId);

0 commit comments

Comments
 (0)