Skip to content

Commit 6e59ef4

Browse files
sarinaliclaude
andauthored
Fix playground model selection and UI improvements (#1030)
* Fix model selection state propagation and persistence - Update chatStateRef synchronously during render instead of in useEffect to prevent stale closure issues when user changes model picker - Use ref in sendAgentRequest to always get latest model/computer - Add user-friendly error messages when model or computer is not selected - Persist model/computer picker changes to storage for existing chats - Fix type errors: use status check directly instead of isVM for ComputerInfo Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com> * Fix new chat creation to include computer When creating a new chat from the sidebar, include the selected computer so the chat can be used immediately without manually selecting a sandbox. Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com> * Prefer running VM when initializing playground When no VM is explicitly selected, prefer the first running VM over the first VM in the list. This ensures the iframe displays a usable VM. Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com> * Fix VM status banner to use selected computer from dropdown Pass the currently selected computer from the dropdown to the VM status banner, so it updates immediately when user selects an offline sandbox. Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com> * Improve ReplayTrajectoryModal UI for compact states Use smaller modal size when showing loading, empty, or no-screenshot states instead of always using the large viewer size. Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com> * Fix empty state and existing chat model/computer propagation - Pass model and computer from empty state picker to chat creation - Sync computer from global state when chat doesn't have one stored - Ensures existing chats without stored computer use the currently selected computer from the dropdown Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
1 parent ed13267 commit 6e59ef4

File tree

10 files changed

+224
-61
lines changed

10 files changed

+224
-61
lines changed

libs/typescript/playground/src/components/Playground.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,25 @@ function PlaygroundContentInternal({
8888
);
8989

9090
// Handle creating a new chat and sending the first message
91-
const handleCreateAndSend = async (message: string) => {
91+
const handleCreateAndSend = async (
92+
message: string,
93+
selectedModel?: Chat['model'],
94+
selectedComputer?: Chat['computer']
95+
) => {
96+
// Use model/computer from empty state picker, or fall back to defaults
97+
const model = selectedModel ?? defaultModel;
98+
const computer =
99+
selectedComputer ??
100+
(currentComputer
101+
? { id: currentComputer.id, name: currentComputer.name, url: currentComputer.agentUrl }
102+
: undefined);
103+
92104
const newChat: Chat = {
93105
id: crypto.randomUUID(),
94106
name: message.slice(0, 50) || 'New Chat',
95107
messages: [],
96-
model: defaultModel,
108+
model,
109+
computer,
97110
created: new Date(),
98111
updated: new Date(),
99112
};

libs/typescript/playground/src/components/composed/ChatContent.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,14 +176,16 @@ export function ChatContent({
176176
};
177177

178178
// Render VM status banner if provided
179+
// Use selectedComputerForInput to show the banner for the currently selected computer in dropdown
180+
// This ensures the banner updates immediately when user selects an offline VM
179181
const vmStatusBanner = renderVMStatusBanner?.({
180182
onRestartVM,
181183
onStartVM,
182184
hasOrg,
183185
hasWorkspace,
184186
hasCredits,
185187
orgSlug,
186-
computer,
188+
computer: selectedComputerForInput,
187189
computers,
188190
});
189191

libs/typescript/playground/src/components/composed/ChatList.tsx

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
useIsChatGenerating,
2626
usePlayground,
2727
} from '../../hooks/usePlayground';
28-
import type { Chat } from '../../types';
28+
import type { Chat, Computer } from '../../types';
2929
import { cn } from '../../utils/cn';
3030

3131
interface ChatListProps {
@@ -51,7 +51,7 @@ export function ChatList({
5151
onToast,
5252
}: ChatListProps) {
5353
const { state, dispatch, adapters } = usePlayground();
54-
const { chats, activeChatId } = state;
54+
const { chats, activeChatId, computers, currentComputerId } = state;
5555
const activeChat = useActiveChat();
5656
const defaultModel = useFindDefaultModel();
5757
const [isCreating, setIsCreating] = useState(false);
@@ -67,11 +67,35 @@ export function ChatList({
6767
setIsCreating(true);
6868

6969
try {
70+
// Priority: use currently selected computer (currentComputerId), then fall back to first available
71+
const selectedComputer = currentComputerId
72+
? computers.find((c) => c.id === currentComputerId)
73+
: undefined;
74+
const computerInfo = selectedComputer ?? (computers.length > 0 ? computers[0] : undefined);
75+
76+
// Check if the computer is stopped
77+
if (computerInfo && computerInfo.status === 'stopped') {
78+
onToast?.('Cannot create chat: The sandbox is stopped. Please start it first.', 'error');
79+
setIsCreating(false);
80+
return;
81+
}
82+
83+
// Convert ComputerInfo to Computer type for the chat
84+
let computer: Computer | undefined;
85+
if (computerInfo) {
86+
const { agentUrl, ...rest } = computerInfo;
87+
computer = {
88+
...rest,
89+
url: agentUrl,
90+
} as Computer;
91+
}
92+
7093
// Create local chat object
7194
const newChat: Chat = {
7295
id: crypto.randomUUID(), // Temporary ID, adapter may replace it
7396
name: 'New Chat',
7497
messages: [],
98+
computer,
7599
model: defaultModel,
76100
created: new Date(),
77101
updated: new Date(),
@@ -91,7 +115,16 @@ export function ChatList({
91115
} finally {
92116
setIsCreating(false);
93117
}
94-
}, [defaultModel, dispatch, isCreating, adapters.persistence, onCreateChat, onToast]);
118+
}, [
119+
computers,
120+
currentComputerId,
121+
defaultModel,
122+
dispatch,
123+
isCreating,
124+
adapters.persistence,
125+
onCreateChat,
126+
onToast,
127+
]);
95128

96129
const handleDeleteConfirm = async () => {
97130
if (!deleteConfirmChat) return;

libs/typescript/playground/src/components/composed/ChatSidebar.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import {
2626
usePlayground,
2727
} from '../../hooks/usePlayground';
2828
import type { Chat, Computer } from '../../types';
29-
import { isVM } from '../../types';
3029
import { cn } from '../../utils/cn';
3130

3231
interface ChatSidebarProps {
@@ -76,8 +75,8 @@ export function ChatSidebar({
7675
const computer = selectedComputer ?? (computers.length > 0 ? computers[0] : undefined);
7776

7877
// Check if the computer is stopped
79-
if (computer && isVM(computer) && computer.status === 'stopped') {
80-
onToast?.('Cannot create chat: The sandbox is stopped. Please start it first.', 'error');
78+
if (computer && computer.status === 'stopped') {
79+
onToast?.('Cannot create chat: The sandbox is stopped. Please start it first.');
8180
return;
8281
}
8382

@@ -116,7 +115,15 @@ export function ChatSidebar({
116115
} finally {
117116
setIsCreating(false);
118117
}
119-
}, [computers, currentComputerId, defaultModel, dispatch, isCreating, adapters.persistence, onToast]);
118+
}, [
119+
computers,
120+
currentComputerId,
121+
defaultModel,
122+
dispatch,
123+
isCreating,
124+
adapters.persistence,
125+
onToast,
126+
]);
120127

121128
const handleDeleteConfirm = async () => {
122129
if (!deleteConfirmChat) return;

libs/typescript/playground/src/components/composed/EmptyState.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import type { Chat, Computer } from '../../types';
1111

1212
interface EmptyStateWithInputProps {
1313
/** Callback when user sends a message to create a new chat */
14-
onCreateAndSend: (message: string) => Promise<void>;
14+
onCreateAndSend: (
15+
message: string,
16+
model?: Chat['model'],
17+
computer?: Chat['computer']
18+
) => Promise<void>;
1519
/** Whether in mobile layout */
1620
isMobile: boolean;
1721
/** Draft chat for UI consistency */
@@ -61,7 +65,11 @@ export function EmptyStateWithInput({
6165
}
6266

6367
interface EmptyStateContentProps {
64-
onCreateAndSend: (message: string) => Promise<void>;
68+
onCreateAndSend: (
69+
message: string,
70+
model?: Chat['model'],
71+
computer?: Chat['computer']
72+
) => Promise<void>;
6573
isMobile: boolean;
6674
logo?: {
6775
lightSrc: string;
@@ -95,7 +103,8 @@ function EmptyStateContent({
95103
if (!chatState.currentInput.trim() || isCreating) return;
96104
setIsCreating(true);
97105
try {
98-
await onCreateAndSend(chatState.currentInput.trim());
106+
// Pass the model and computer selected in the empty state picker
107+
await onCreateAndSend(chatState.currentInput.trim(), chatState.model, chatState.computer);
99108
chatDispatch({ type: 'CLEAR_INPUT' });
100109
} catch (error) {
101110
console.error('Failed to create chat:', error);

libs/typescript/playground/src/components/composed/PlaygroundContent.tsx

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
usePlayground,
1414
} from '../../hooks/usePlayground';
1515
import type { Chat, Computer } from '../../types';
16-
import { isVM } from '../../types';
1716

1817
// Re-export for convenience
1918
export { EmptyStateWithInput } from './EmptyState';
@@ -148,32 +147,47 @@ export function PlaygroundContent({
148147
}, [initialized, activeChat, defaultModel, dispatch]);
149148

150149
// Create a new chat and send the first message
151-
const handleCreateChatWithMessage = async (message: string) => {
152-
// Priority: use currently selected computer (currentComputerId), then fall back to first running, then first available
153-
const selectedComputer = state.currentComputerId
154-
? computers.find((c) => c.id === state.currentComputerId)
155-
: undefined;
156-
const runningComputer = computers.find((c) => c.status === 'running');
157-
const computerInfo = selectedComputer ?? runningComputer ?? computers[0];
158-
159-
if (!computerInfo) {
160-
onToast?.('Please select a sandbox to interact with.', 'error');
161-
return;
162-
}
163-
164-
// Check if the selected computer is stopped
165-
if (isVM(computerInfo) && computerInfo.status === 'stopped') {
166-
onToast?.('Cannot start chat: The selected sandbox is stopped. Please start it first.', 'error');
167-
return;
150+
const handleCreateChatWithMessage = async (
151+
message: string,
152+
selectedModel?: Chat['model'],
153+
selectedChatComputer?: Chat['computer']
154+
) => {
155+
// Use the model passed from empty state picker, or fall back to default
156+
const model = selectedModel ?? defaultModel;
157+
158+
// Use computer from empty state picker, or fall back to global selection
159+
let computer: Computer | undefined = selectedChatComputer;
160+
161+
if (!computer) {
162+
// Fall back to global computer selection
163+
const selectedComputer = state.currentComputerId
164+
? computers.find((c) => c.id === state.currentComputerId)
165+
: undefined;
166+
const runningComputer = computers.find((c) => c.status === 'running');
167+
const computerInfo = selectedComputer ?? runningComputer ?? computers[0];
168+
169+
if (!computerInfo) {
170+
onToast?.('Please select a sandbox to interact with.', 'error');
171+
return;
172+
}
173+
174+
// Check if the selected computer is stopped
175+
if (computerInfo.status === 'stopped') {
176+
onToast?.(
177+
'Cannot start chat: The selected sandbox is stopped. Please start it first.',
178+
'error'
179+
);
180+
return;
181+
}
182+
183+
// Convert ComputerInfo to Computer
184+
computer = {
185+
id: computerInfo.id,
186+
name: computerInfo.name,
187+
url: computerInfo.agentUrl,
188+
};
168189
}
169190

170-
// Convert ComputerInfo to Computer
171-
const computer: Computer = {
172-
id: computerInfo.id,
173-
name: computerInfo.name,
174-
url: computerInfo.agentUrl,
175-
};
176-
177191
try {
178192
// Create chat title from first message
179193
const base = message.trim().replace(/\s+/g, ' ');
@@ -192,7 +206,7 @@ export function PlaygroundContent({
192206
name: title,
193207
messages: [userMessage],
194208
computer,
195-
model: defaultModel,
209+
model,
196210
created: new Date(),
197211
updated: new Date(),
198212
};

libs/typescript/playground/src/components/modals/ReplayTrajectoryModal.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,17 @@ export default function ReplayTrajectoryModal({
173173

174174
const selectedRun = runs.find((r) => r.index === selectedRunIndex);
175175

176+
// Use compact size for loading, empty, or no-screenshot states
177+
const hasScreenshots = selectedRun && selectedRun.screenshotCount > 0 && zipUrl;
178+
const isCompactView = loading || runs.length === 0 || generatingZip || !hasScreenshots;
179+
180+
const modalSizeClass = isCompactView ? 'w-full max-w-md' : 'h-[90vh] w-[95vw] max-w-7xl';
181+
176182
return (
177183
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
178-
<div className="relative flex h-[90vh] w-[95vw] max-w-7xl flex-col overflow-hidden rounded-lg bg-white shadow-xl dark:bg-neutral-900">
184+
<div
185+
className={`relative flex flex-col overflow-hidden rounded-lg bg-white shadow-xl dark:bg-neutral-900 ${modalSizeClass}`}
186+
>
179187
{/* Header */}
180188
<div className="flex items-center justify-between border-neutral-200 border-b px-4 py-3 dark:border-neutral-700">
181189
<div className="flex items-center gap-3">
@@ -218,9 +226,9 @@ export default function ReplayTrajectoryModal({
218226
)}
219227

220228
{/* Main content */}
221-
<div className="flex-1 overflow-hidden">
229+
<div className={isCompactView ? 'p-6' : 'flex-1 overflow-hidden'}>
222230
{loading ? (
223-
<div className="flex h-full items-center justify-center">
231+
<div className="flex items-center justify-center py-8">
224232
<div className="flex flex-col items-center gap-3">
225233
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
226234
<p className="text-neutral-600 text-sm dark:text-neutral-400">
@@ -229,13 +237,13 @@ export default function ReplayTrajectoryModal({
229237
</div>
230238
</div>
231239
) : runs.length === 0 ? (
232-
<div className="flex h-full items-center justify-center">
240+
<div className="flex items-center justify-center py-8">
233241
<p className="text-neutral-500 dark:text-neutral-400">
234242
No trajectory runs found in this chat.
235243
</p>
236244
</div>
237245
) : generatingZip ? (
238-
<div className="flex h-full items-center justify-center">
246+
<div className="flex items-center justify-center py-8">
239247
<div className="flex flex-col items-center gap-3">
240248
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
241249
<p className="text-neutral-600 text-sm dark:text-neutral-400">
@@ -244,7 +252,7 @@ export default function ReplayTrajectoryModal({
244252
</div>
245253
</div>
246254
) : selectedRun && selectedRun.screenshotCount === 0 ? (
247-
<div className="flex h-full items-center justify-center">
255+
<div className="flex items-center justify-center py-8">
248256
<div className="text-center">
249257
<p className="text-neutral-500 dark:text-neutral-400">
250258
This run has no screenshots to replay.
@@ -270,8 +278,8 @@ export default function ReplayTrajectoryModal({
270278
) : null}
271279
</div>
272280

273-
{/* Footer with run info */}
274-
{selectedRun && !loading && (
281+
{/* Footer with run info - only show when viewing actual trajectory */}
282+
{selectedRun && !loading && !isCompactView && (
275283
<div className="flex items-center justify-between border-neutral-200 border-t bg-neutral-50 px-4 py-2 text-xs dark:border-neutral-700 dark:bg-neutral-800/50">
276284
<span className="text-neutral-500 dark:text-neutral-400">
277285
{selectedRun.screenshotCount} screenshot

0 commit comments

Comments
 (0)