From b2afc53e1e66db20a932ba9fe1a9639ae7f4314e Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Fri, 15 May 2026 22:54:31 +0400 Subject: [PATCH] studio/chat: surface unpersisted-thread picker no-op as a toast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Active for this thread" container picker uses db.threads.update(activeThreadId, ...), which silently returns 0 rows affected when the thread record isn't yet in IndexedDB. That happens on a brand-new thread where the user toggles code execution on and opens settings before sending the first message — the chat adapter only materializes the thread row on first send. The picker would appear to ignore the user's selection and snap back to "Auto-create per thread". - onPick now awaits the update and toasts an actionable hint ("Send a message first to pin a container to this thread.") when the update affected zero rows. - Auto-bind effect comment clarifies why it stays best-effort silent. The auto-bind effect itself is unchanged: it's a heuristic that should not nag the user when it can't apply. --- .../components/openai-code-exec-section.tsx | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/studio/frontend/src/features/chat/components/openai-code-exec-section.tsx b/studio/frontend/src/features/chat/components/openai-code-exec-section.tsx index 1272765c92..4e8e59f003 100644 --- a/studio/frontend/src/features/chat/components/openai-code-exec-section.tsx +++ b/studio/frontend/src/features/chat/components/openai-code-exec-section.tsx @@ -121,6 +121,12 @@ export function OpenAICodeExecSection({ // with what feels "most recent" from the user's perspective. The // user can still pick "Auto-create per thread" explicitly to start // fresh. + // + // Dexie's `update()` returns 0 affected rows when the thread record + // is not yet persisted (empty-state composer, no message sent). We + // silently ignore that here because auto-bind is best-effort; the + // user-initiated `onPick` below surfaces the same condition as a + // toast so the user understands why the picker didn't stick. useEffect(() => { if (!activeThreadId || activeContainerId || containers.length === 0) { return; @@ -146,12 +152,29 @@ export function OpenAICodeExecSection({ onProviderChange({ ...provider, openaiContainerTtlMinutes: clamped }); }; - const onPick = (value: string) => { + const onPick = async (value: string) => { if (!activeThreadId) return; const next = value === AUTO_OPTION_VALUE ? null : value; - void db.threads - .update(activeThreadId, { openaiCodeExecContainerId: next }) - .catch(() => {}); + // Dexie's `update` returns the count of rows changed: 0 means the + // thread record is not yet in IndexedDB. That happens when the user + // is on a brand-new thread before sending the first message — the + // thread row is materialized on first send by the chat adapter. + // Without this guard the selection silently no-ops and the picker + // snaps back to "Auto-create per thread", which looks broken. + try { + const affected = await db.threads.update(activeThreadId, { + openaiCodeExecContainerId: next, + }); + if (affected === 0) { + toast.error( + "Send a message first to pin a container to this thread.", + ); + } + } catch (err) { + toast.error( + `Could not update thread: ${err instanceof Error ? err.message : "Unknown"}`, + ); + } }; const onCreate = async () => {