Skip to content

Commit 8f544d5

Browse files
authored
Prettify messages (#606)
- Adds shimmer to reasoning - Restricts the max width for the input and - Aligns user messages to the right - Only puts meta buttons on last message View the Storybook: https://68e30fca49979473fc9abc73-iryztuiwlu.chromatic.com/iframe.html?viewMode=story&id=app-full-application--active-workspace-with-chat&args=&globals=#workspace=demo-workspace <img width="2195" height="1336" alt="image" src="https://github.com/user-attachments/assets/5d4903bb-4b02-48d6-a11c-11d3197727b9" />
1 parent 0ad0cef commit 8f544d5

File tree

11 files changed

+437
-385
lines changed

11 files changed

+437
-385
lines changed

src/App.stories.tsx

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,7 @@ export const ActiveWorkspaceWithChat: Story = {
590590
metadata: {
591591
historySequence: 2,
592592
timestamp: STABLE_TIMESTAMP - 290000,
593-
model: "claude-sonnet-4-20250514",
593+
model: "anthropic:claude-sonnet-4-5",
594594
usage: {
595595
inputTokens: 1250,
596596
outputTokens: 450,
@@ -640,7 +640,7 @@ export const ActiveWorkspaceWithChat: Story = {
640640
metadata: {
641641
historySequence: 4,
642642
timestamp: STABLE_TIMESTAMP - 270000,
643-
model: "claude-sonnet-4-20250514",
643+
model: "anthropic:claude-sonnet-4-5",
644644
usage: {
645645
inputTokens: 2100,
646646
outputTokens: 680,
@@ -663,7 +663,7 @@ export const ActiveWorkspaceWithChat: Story = {
663663
metadata: {
664664
historySequence: 5,
665665
timestamp: STABLE_TIMESTAMP - 260000,
666-
model: "claude-sonnet-4-20250514",
666+
model: "anthropic:claude-sonnet-4-5",
667667
usage: {
668668
inputTokens: 1800,
669669
outputTokens: 520,
@@ -715,7 +715,7 @@ export const ActiveWorkspaceWithChat: Story = {
715715
metadata: {
716716
historySequence: 7,
717717
timestamp: STABLE_TIMESTAMP - 230000,
718-
model: "claude-sonnet-4-20250514",
718+
model: "anthropic:claude-sonnet-4-5",
719719
usage: {
720720
inputTokens: 2800,
721721
outputTokens: 420,
@@ -775,7 +775,7 @@ export const ActiveWorkspaceWithChat: Story = {
775775
metadata: {
776776
historySequence: 9,
777777
timestamp: STABLE_TIMESTAMP - 170000,
778-
model: "claude-sonnet-4-20250514",
778+
model: "anthropic:claude-sonnet-4-5",
779779
usage: {
780780
inputTokens: 3500,
781781
outputTokens: 520,
@@ -816,7 +816,7 @@ export const ActiveWorkspaceWithChat: Story = {
816816
metadata: {
817817
historySequence: 10,
818818
timestamp: STABLE_TIMESTAMP - 160000,
819-
model: "claude-sonnet-4-20250514",
819+
model: "anthropic:claude-sonnet-4-5",
820820
usage: {
821821
inputTokens: 800,
822822
outputTokens: 150,
@@ -826,12 +826,60 @@ export const ActiveWorkspaceWithChat: Story = {
826826
},
827827
});
828828

829+
// User follow-up asking about documentation
830+
callback({
831+
id: "msg-11",
832+
role: "user",
833+
parts: [
834+
{
835+
type: "text",
836+
text: "Should we add documentation for the authentication changes?",
837+
},
838+
],
839+
metadata: {
840+
historySequence: 11,
841+
timestamp: STABLE_TIMESTAMP - 150000,
842+
},
843+
});
844+
829845
// Mark as caught up
830846
callback({ type: "caught-up" });
847+
848+
// Now start streaming assistant response with reasoning
849+
callback({
850+
type: "stream-start",
851+
workspaceId: workspaceId,
852+
messageId: "msg-12",
853+
model: "anthropic:claude-sonnet-4-5",
854+
historySequence: 12,
855+
});
856+
857+
// Send reasoning delta
858+
callback({
859+
type: "reasoning-delta",
860+
workspaceId: workspaceId,
861+
messageId: "msg-12",
862+
delta:
863+
"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.",
864+
tokens: 65,
865+
timestamp: STABLE_TIMESTAMP - 140000,
866+
});
831867
}, 100);
832868

869+
// Keep sending reasoning deltas to maintain streaming state
870+
const intervalId = setInterval(() => {
871+
callback({
872+
type: "reasoning-delta",
873+
workspaceId: workspaceId,
874+
messageId: "msg-12",
875+
delta: ".",
876+
tokens: 1,
877+
timestamp: NOW,
878+
});
879+
}, 2000);
880+
833881
return () => {
834-
// Cleanup
882+
clearInterval(intervalId);
835883
};
836884
} else if (wsId === streamingWorkspaceId) {
837885
// Streaming workspace - show active work in progress
@@ -866,7 +914,7 @@ export const ActiveWorkspaceWithChat: Story = {
866914
metadata: {
867915
historySequence: 0,
868916
timestamp: now - 5000, // 5 seconds ago
869-
model: "claude-sonnet-4-20250514",
917+
model: "anthropic:claude-sonnet-4-5",
870918
usage: {
871919
inputTokens: 200,
872920
outputTokens: 50,
@@ -902,7 +950,7 @@ export const ActiveWorkspaceWithChat: Story = {
902950
type: "stream-start",
903951
workspaceId: streamingWorkspaceId,
904952
messageId: "stream-msg-2",
905-
model: "claude-sonnet-4-20250514",
953+
model: "anthropic:claude-sonnet-4-5",
906954
historySequence: 2,
907955
});
908956

@@ -1227,7 +1275,7 @@ These tables should render cleanly without any disruptive copy or download actio
12271275
metadata: {
12281276
historySequence: 2,
12291277
timestamp: STABLE_TIMESTAMP + 1000,
1230-
model: "claude-sonnet-4-20250514",
1278+
model: "anthropic:claude-sonnet-4-5",
12311279
usage: {
12321280
inputTokens: 100,
12331281
outputTokens: 500,

src/components/AIView.tsx

Lines changed: 87 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -374,94 +374,96 @@ const AIViewInner: React.FC<AIViewProps> = ({
374374
tabIndex={0}
375375
className="h-full overflow-y-auto p-[15px] leading-[1.5] break-words whitespace-pre-wrap"
376376
>
377-
{mergedMessages.length === 0 ? (
378-
<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]">
379-
<h3>No Messages Yet</h3>
380-
<p>Send a message below to begin</p>
381-
<p className="mt-5 text-xs text-[#888]">
382-
💡 Tip: Add a{" "}
383-
<code className="rounded-[3px] bg-[#2d2d30] px-1.5 py-0.5 font-mono text-[11px] text-[#d7ba7d]">
384-
.cmux/init
385-
</code>{" "}
386-
hook to your project to run setup commands
387-
<br />
388-
(e.g., install dependencies, build) when creating new workspaces
389-
</p>
390-
</div>
391-
) : (
392-
<>
393-
{mergedMessages.map((msg) => {
394-
const isAtCutoff =
395-
editCutoffHistoryId !== undefined &&
396-
msg.type !== "history-hidden" &&
397-
msg.type !== "workspace-init" &&
398-
msg.historyId === editCutoffHistoryId;
399-
400-
return (
401-
<React.Fragment key={msg.id}>
402-
<div
403-
data-message-id={
404-
msg.type !== "history-hidden" && msg.type !== "workspace-init"
405-
? msg.historyId
406-
: undefined
407-
}
408-
>
409-
<MessageRenderer
410-
message={msg}
411-
onEditUserMessage={handleEditUserMessage}
412-
workspaceId={workspaceId}
413-
isCompacting={isCompacting}
414-
/>
415-
</div>
416-
{isAtCutoff && (
377+
<div className={cn("max-w-4xl mx-auto", mergedMessages.length === 0 && "h-full")}>
378+
{mergedMessages.length === 0 ? (
379+
<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]">
380+
<h3>No Messages Yet</h3>
381+
<p>Send a message below to begin</p>
382+
<p className="mt-5 text-xs text-[#888]">
383+
💡 Tip: Add a{" "}
384+
<code className="rounded-[3px] bg-[#2d2d30] px-1.5 py-0.5 font-mono text-[11px] text-[#d7ba7d]">
385+
.cmux/init
386+
</code>{" "}
387+
hook to your project to run setup commands
388+
<br />
389+
(e.g., install dependencies, build) when creating new workspaces
390+
</p>
391+
</div>
392+
) : (
393+
<>
394+
{mergedMessages.map((msg) => {
395+
const isAtCutoff =
396+
editCutoffHistoryId !== undefined &&
397+
msg.type !== "history-hidden" &&
398+
msg.type !== "workspace-init" &&
399+
msg.historyId === editCutoffHistoryId;
400+
401+
return (
402+
<React.Fragment key={msg.id}>
417403
<div
418-
className="text-edit-mode bg-edit-mode/10 my-5 px-[15px] py-3 text-center text-xs font-medium"
419-
style={{
420-
borderBottom: "3px solid",
421-
borderImage:
422-
"repeating-linear-gradient(45deg, var(--color-editing-mode), var(--color-editing-mode) 10px, transparent 10px, transparent 20px) 1",
423-
}}
404+
data-message-id={
405+
msg.type !== "history-hidden" && msg.type !== "workspace-init"
406+
? msg.historyId
407+
: undefined
408+
}
424409
>
425-
⚠️ Messages below this line will be removed when you submit the edit
410+
<MessageRenderer
411+
message={msg}
412+
onEditUserMessage={handleEditUserMessage}
413+
workspaceId={workspaceId}
414+
isCompacting={isCompacting}
415+
/>
426416
</div>
427-
)}
428-
{shouldShowInterruptedBarrier(msg) && <InterruptedBarrier />}
429-
</React.Fragment>
430-
);
431-
})}
432-
{/* Show RetryBarrier after the last message if needed */}
433-
{showRetryBarrier && <RetryBarrier workspaceId={workspaceId} />}
434-
</>
435-
)}
436-
<PinnedTodoList workspaceId={workspaceId} />
437-
{canInterrupt && (
438-
<StreamingBarrier
439-
statusText={
440-
isCompacting
441-
? currentModel
442-
? `${getModelName(currentModel)} compacting...`
443-
: "compacting..."
444-
: currentModel
445-
? `${getModelName(currentModel)} streaming...`
446-
: "streaming..."
447-
}
448-
cancelText={
449-
isCompacting
450-
? `${formatKeybind(vimEnabled ? KEYBINDS.INTERRUPT_STREAM_VIM : KEYBINDS.INTERRUPT_STREAM_NORMAL)} cancel | ${formatKeybind(KEYBINDS.ACCEPT_EARLY_COMPACTION)} accept early`
451-
: `hit ${formatKeybind(vimEnabled ? KEYBINDS.INTERRUPT_STREAM_VIM : KEYBINDS.INTERRUPT_STREAM_NORMAL)} to cancel`
452-
}
453-
tokenCount={
454-
activeStreamMessageId
455-
? aggregator.getStreamingTokenCount(activeStreamMessageId)
456-
: undefined
457-
}
458-
tps={
459-
activeStreamMessageId
460-
? aggregator.getStreamingTPS(activeStreamMessageId)
461-
: undefined
462-
}
463-
/>
464-
)}
417+
{isAtCutoff && (
418+
<div
419+
className="text-edit-mode bg-edit-mode/10 my-5 px-[15px] py-3 text-center text-xs font-medium"
420+
style={{
421+
borderBottom: "3px solid",
422+
borderImage:
423+
"repeating-linear-gradient(45deg, var(--color-editing-mode), var(--color-editing-mode) 10px, transparent 10px, transparent 20px) 1",
424+
}}
425+
>
426+
⚠️ Messages below this line will be removed when you submit the edit
427+
</div>
428+
)}
429+
{shouldShowInterruptedBarrier(msg) && <InterruptedBarrier />}
430+
</React.Fragment>
431+
);
432+
})}
433+
{/* Show RetryBarrier after the last message if needed */}
434+
{showRetryBarrier && <RetryBarrier workspaceId={workspaceId} />}
435+
</>
436+
)}
437+
<PinnedTodoList workspaceId={workspaceId} />
438+
{canInterrupt && (
439+
<StreamingBarrier
440+
statusText={
441+
isCompacting
442+
? currentModel
443+
? `${getModelName(currentModel)} compacting...`
444+
: "compacting..."
445+
: currentModel
446+
? `${getModelName(currentModel)} streaming...`
447+
: "streaming..."
448+
}
449+
cancelText={
450+
isCompacting
451+
? `${formatKeybind(vimEnabled ? KEYBINDS.INTERRUPT_STREAM_VIM : KEYBINDS.INTERRUPT_STREAM_NORMAL)} cancel | ${formatKeybind(KEYBINDS.ACCEPT_EARLY_COMPACTION)} accept early`
452+
: `hit ${formatKeybind(vimEnabled ? KEYBINDS.INTERRUPT_STREAM_VIM : KEYBINDS.INTERRUPT_STREAM_NORMAL)} to cancel`
453+
}
454+
tokenCount={
455+
activeStreamMessageId
456+
? aggregator.getStreamingTokenCount(activeStreamMessageId)
457+
: undefined
458+
}
459+
tps={
460+
activeStreamMessageId
461+
? aggregator.getStreamingTPS(activeStreamMessageId)
462+
: undefined
463+
}
464+
/>
465+
)}
466+
</div>
465467
</div>
466468
{!autoScroll && (
467469
<button
@@ -487,7 +489,6 @@ const AIViewInner: React.FC<AIViewProps> = ({
487489
</button>
488490
)}
489491
</div>
490-
491492
<ChatInput
492493
variant="workspace"
493494
workspaceId={workspaceId}

0 commit comments

Comments
 (0)