Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 58 additions & 10 deletions src/App.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ export const ActiveWorkspaceWithChat: Story = {
metadata: {
historySequence: 2,
timestamp: STABLE_TIMESTAMP - 290000,
model: "claude-sonnet-4-20250514",
model: "anthropic:claude-sonnet-4-5",
usage: {
inputTokens: 1250,
outputTokens: 450,
Expand Down Expand Up @@ -634,7 +634,7 @@ export const ActiveWorkspaceWithChat: Story = {
metadata: {
historySequence: 4,
timestamp: STABLE_TIMESTAMP - 270000,
model: "claude-sonnet-4-20250514",
model: "anthropic:claude-sonnet-4-5",
usage: {
inputTokens: 2100,
outputTokens: 680,
Expand All @@ -657,7 +657,7 @@ export const ActiveWorkspaceWithChat: Story = {
metadata: {
historySequence: 5,
timestamp: STABLE_TIMESTAMP - 260000,
model: "claude-sonnet-4-20250514",
model: "anthropic:claude-sonnet-4-5",
usage: {
inputTokens: 1800,
outputTokens: 520,
Expand Down Expand Up @@ -709,7 +709,7 @@ export const ActiveWorkspaceWithChat: Story = {
metadata: {
historySequence: 7,
timestamp: STABLE_TIMESTAMP - 230000,
model: "claude-sonnet-4-20250514",
model: "anthropic:claude-sonnet-4-5",
usage: {
inputTokens: 2800,
outputTokens: 420,
Expand Down Expand Up @@ -769,7 +769,7 @@ export const ActiveWorkspaceWithChat: Story = {
metadata: {
historySequence: 9,
timestamp: STABLE_TIMESTAMP - 170000,
model: "claude-sonnet-4-20250514",
model: "anthropic:claude-sonnet-4-5",
usage: {
inputTokens: 3500,
outputTokens: 520,
Expand Down Expand Up @@ -810,7 +810,7 @@ export const ActiveWorkspaceWithChat: Story = {
metadata: {
historySequence: 10,
timestamp: STABLE_TIMESTAMP - 160000,
model: "claude-sonnet-4-20250514",
model: "anthropic:claude-sonnet-4-5",
usage: {
inputTokens: 800,
outputTokens: 150,
Expand All @@ -820,12 +820,60 @@ export const ActiveWorkspaceWithChat: Story = {
},
});

// User follow-up asking about documentation
callback({
id: "msg-11",
role: "user",
parts: [
{
type: "text",
text: "Should we add documentation for the authentication changes?",
},
],
metadata: {
historySequence: 11,
timestamp: STABLE_TIMESTAMP - 150000,
},
});

// Mark as caught up
callback({ type: "caught-up" });

// Now start streaming assistant response with reasoning
callback({
type: "stream-start",
workspaceId: workspaceId,
messageId: "msg-12",
model: "anthropic:claude-sonnet-4-5",
historySequence: 12,
});

// Send reasoning delta
callback({
type: "reasoning-delta",
workspaceId: workspaceId,
messageId: "msg-12",
delta:
"The user is asking about documentation. This is important because the authentication changes introduce a breaking change for API clients. They'll need to know how to include JWT tokens in their requests. I should suggest adding both inline code comments and updating the API documentation to explain the new authentication requirements, including examples of how to obtain and use tokens.",
tokens: 65,
timestamp: STABLE_TIMESTAMP - 140000,
});
}, 100);

// Keep sending reasoning deltas to maintain streaming state
const intervalId = setInterval(() => {
callback({
type: "reasoning-delta",
workspaceId: workspaceId,
messageId: "msg-12",
delta: ".",
tokens: 1,
timestamp: NOW,
});
}, 2000);

return () => {
// Cleanup
clearInterval(intervalId);
};
} else if (wsId === streamingWorkspaceId) {
// Streaming workspace - show active work in progress
Expand Down Expand Up @@ -860,7 +908,7 @@ export const ActiveWorkspaceWithChat: Story = {
metadata: {
historySequence: 0,
timestamp: now - 5000, // 5 seconds ago
model: "claude-sonnet-4-20250514",
model: "anthropic:claude-sonnet-4-5",
usage: {
inputTokens: 200,
outputTokens: 50,
Expand Down Expand Up @@ -896,7 +944,7 @@ export const ActiveWorkspaceWithChat: Story = {
type: "stream-start",
workspaceId: streamingWorkspaceId,
messageId: "stream-msg-2",
model: "claude-sonnet-4-20250514",
model: "anthropic:claude-sonnet-4-5",
historySequence: 2,
});

Expand Down Expand Up @@ -1221,7 +1269,7 @@ These tables should render cleanly without any disruptive copy or download actio
metadata: {
historySequence: 2,
timestamp: STABLE_TIMESTAMP + 1000,
model: "claude-sonnet-4-20250514",
model: "anthropic:claude-sonnet-4-5",
usage: {
inputTokens: 100,
outputTokens: 500,
Expand Down
173 changes: 87 additions & 86 deletions src/components/AIView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -374,94 +374,96 @@ const AIViewInner: React.FC<AIViewProps> = ({
tabIndex={0}
className="h-full overflow-y-auto p-[15px] leading-[1.5] break-words whitespace-pre-wrap"
>
{mergedMessages.length === 0 ? (
<div className="text-placeholder flex h-full flex-1 flex-col items-center justify-center text-center [&_h3]:m-0 [&_h3]:mb-2.5 [&_h3]:text-base [&_h3]:font-medium [&_p]:m-0 [&_p]:text-[13px]">
<h3>No Messages Yet</h3>
<p>Send a message below to begin</p>
<p className="mt-5 text-xs text-[#888]">
💡 Tip: Add a{" "}
<code className="rounded-[3px] bg-[#2d2d30] px-1.5 py-0.5 font-mono text-[11px] text-[#d7ba7d]">
.cmux/init
</code>{" "}
hook to your project to run setup commands
<br />
(e.g., install dependencies, build) when creating new workspaces
</p>
</div>
) : (
<>
{mergedMessages.map((msg) => {
const isAtCutoff =
editCutoffHistoryId !== undefined &&
msg.type !== "history-hidden" &&
msg.type !== "workspace-init" &&
msg.historyId === editCutoffHistoryId;

return (
<React.Fragment key={msg.id}>
<div
data-message-id={
msg.type !== "history-hidden" && msg.type !== "workspace-init"
? msg.historyId
: undefined
}
>
<MessageRenderer
message={msg}
onEditUserMessage={handleEditUserMessage}
workspaceId={workspaceId}
isCompacting={isCompacting}
/>
</div>
{isAtCutoff && (
<div className={cn("max-w-4xl mx-auto", mergedMessages.length === 0 && "h-full")}>
{mergedMessages.length === 0 ? (
<div className="text-placeholder flex h-full flex-1 flex-col items-center justify-center text-center [&_h3]:m-0 [&_h3]:mb-2.5 [&_h3]:text-base [&_h3]:font-medium [&_p]:m-0 [&_p]:text-[13px]">
<h3>No Messages Yet</h3>
<p>Send a message below to begin</p>
<p className="mt-5 text-xs text-[#888]">
💡 Tip: Add a{" "}
<code className="rounded-[3px] bg-[#2d2d30] px-1.5 py-0.5 font-mono text-[11px] text-[#d7ba7d]">
.cmux/init
</code>{" "}
hook to your project to run setup commands
<br />
(e.g., install dependencies, build) when creating new workspaces
</p>
</div>
) : (
<>
{mergedMessages.map((msg) => {
const isAtCutoff =
editCutoffHistoryId !== undefined &&
msg.type !== "history-hidden" &&
msg.type !== "workspace-init" &&
msg.historyId === editCutoffHistoryId;

return (
<React.Fragment key={msg.id}>
<div
className="text-edit-mode bg-edit-mode/10 my-5 px-[15px] py-3 text-center text-xs font-medium"
style={{
borderBottom: "3px solid",
borderImage:
"repeating-linear-gradient(45deg, var(--color-editing-mode), var(--color-editing-mode) 10px, transparent 10px, transparent 20px) 1",
}}
data-message-id={
msg.type !== "history-hidden" && msg.type !== "workspace-init"
? msg.historyId
: undefined
}
>
⚠️ Messages below this line will be removed when you submit the edit
<MessageRenderer
message={msg}
onEditUserMessage={handleEditUserMessage}
workspaceId={workspaceId}
isCompacting={isCompacting}
/>
</div>
)}
{shouldShowInterruptedBarrier(msg) && <InterruptedBarrier />}
</React.Fragment>
);
})}
{/* Show RetryBarrier after the last message if needed */}
{showRetryBarrier && <RetryBarrier workspaceId={workspaceId} />}
</>
)}
<PinnedTodoList workspaceId={workspaceId} />
{canInterrupt && (
<StreamingBarrier
statusText={
isCompacting
? currentModel
? `${getModelName(currentModel)} compacting...`
: "compacting..."
: currentModel
? `${getModelName(currentModel)} streaming...`
: "streaming..."
}
cancelText={
isCompacting
? `${formatKeybind(vimEnabled ? KEYBINDS.INTERRUPT_STREAM_VIM : KEYBINDS.INTERRUPT_STREAM_NORMAL)} cancel | ${formatKeybind(KEYBINDS.ACCEPT_EARLY_COMPACTION)} accept early`
: `hit ${formatKeybind(vimEnabled ? KEYBINDS.INTERRUPT_STREAM_VIM : KEYBINDS.INTERRUPT_STREAM_NORMAL)} to cancel`
}
tokenCount={
activeStreamMessageId
? aggregator.getStreamingTokenCount(activeStreamMessageId)
: undefined
}
tps={
activeStreamMessageId
? aggregator.getStreamingTPS(activeStreamMessageId)
: undefined
}
/>
)}
{isAtCutoff && (
<div
className="text-edit-mode bg-edit-mode/10 my-5 px-[15px] py-3 text-center text-xs font-medium"
style={{
borderBottom: "3px solid",
borderImage:
"repeating-linear-gradient(45deg, var(--color-editing-mode), var(--color-editing-mode) 10px, transparent 10px, transparent 20px) 1",
}}
>
⚠️ Messages below this line will be removed when you submit the edit
</div>
)}
{shouldShowInterruptedBarrier(msg) && <InterruptedBarrier />}
</React.Fragment>
);
})}
{/* Show RetryBarrier after the last message if needed */}
{showRetryBarrier && <RetryBarrier workspaceId={workspaceId} />}
</>
)}
<PinnedTodoList workspaceId={workspaceId} />
{canInterrupt && (
<StreamingBarrier
statusText={
isCompacting
? currentModel
? `${getModelName(currentModel)} compacting...`
: "compacting..."
: currentModel
? `${getModelName(currentModel)} streaming...`
: "streaming..."
}
cancelText={
isCompacting
? `${formatKeybind(vimEnabled ? KEYBINDS.INTERRUPT_STREAM_VIM : KEYBINDS.INTERRUPT_STREAM_NORMAL)} cancel | ${formatKeybind(KEYBINDS.ACCEPT_EARLY_COMPACTION)} accept early`
: `hit ${formatKeybind(vimEnabled ? KEYBINDS.INTERRUPT_STREAM_VIM : KEYBINDS.INTERRUPT_STREAM_NORMAL)} to cancel`
}
tokenCount={
activeStreamMessageId
? aggregator.getStreamingTokenCount(activeStreamMessageId)
: undefined
}
tps={
activeStreamMessageId
? aggregator.getStreamingTPS(activeStreamMessageId)
: undefined
}
/>
)}
</div>
</div>
{!autoScroll && (
<button
Expand All @@ -487,7 +489,6 @@ const AIViewInner: React.FC<AIViewProps> = ({
</button>
)}
</div>

<ChatInput
variant="workspace"
workspaceId={workspaceId}
Expand Down
Loading