diff --git a/studio/frontend/src/features/chat/api/chat-adapter.ts b/studio/frontend/src/features/chat/api/chat-adapter.ts index f141eb8bc1..c0e09febb7 100644 --- a/studio/frontend/src/features/chat/api/chat-adapter.ts +++ b/studio/frontend/src/features/chat/api/chat-adapter.ts @@ -1002,12 +1002,47 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter { } catch { openaiCodeExecContainerId = null; } + // Cross-thread inheritance: when the active thread has + // no container yet, default to the one most recently + // used on *any* other thread (provider-scoped). + // Matches what the Code Execution settings section + // shows in the picker, and keeps the user from getting + // a fresh container on every new thread. The picker + // can still be set to "Auto-create per thread" + // explicitly to opt into a fresh container — but + // that's done via the dropdown, not silently. + if ( + !openaiCodeExecContainerId && + externalProvider.providerType === "openai" + ) { + try { + const others = await db.threads + .orderBy("createdAt") + .reverse() + .toArray(); + for (const t of others) { + if (t.id === resolvedThreadId) continue; + if (t.openaiCodeExecContainerId) { + openaiCodeExecContainerId = t.openaiCodeExecContainerId; + void db.threads + .update(resolvedThreadId, { + openaiCodeExecContainerId, + }) + .catch(() => {}); + break; + } + } + } catch { + /* fall through to lazy-create below */ + } + } // Lazy pre-create when the user has configured a non- - // default TTL: we POST /v1/containers ourselves with - // that TTL so the auto-create-per-thread path actually - // honors the user's preference. The default 20-min - // container_auto path stays as-is (no extra round trip) - // when TTL is unset or matches OpenAI's default. + // default TTL and there's no inherited container: we + // POST /v1/containers ourselves with that TTL so the + // auto-create-per-thread path actually honors the + // user's preference. The default 20-min container_auto + // path stays as-is (no extra round trip) when TTL is + // unset or matches OpenAI's default. if ( !openaiCodeExecContainerId && externalProvider.providerType === "openai" 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 4b2f83ab84..1272765c92 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 @@ -113,6 +113,30 @@ export function OpenAICodeExecSection({ void refresh(); }, [refresh]); + // Auto-bind the active thread to the most-recently-active container + // when the thread has none set. Mirrors the chat-adapter's cross- + // thread inheritance so the picker shows the same default the + // backend would use, and so the user doesn't have to manually + // re-pick on every new thread. Sorting by `lastActiveAt` lines up + // with what feels "most recent" from the user's perspective. The + // user can still pick "Auto-create per thread" explicitly to start + // fresh. + useEffect(() => { + if (!activeThreadId || activeContainerId || containers.length === 0) { + return; + } + const sorted = [...containers].sort( + (a, b) => (b.lastActiveAt ?? 0) - (a.lastActiveAt ?? 0), + ); + const candidate = sorted[0]; + if (!candidate) return; + void db.threads + .update(activeThreadId, { + openaiCodeExecContainerId: candidate.id, + }) + .catch(() => {}); + }, [activeThreadId, activeContainerId, containers]); + const ttlValue = provider.openaiContainerTtlMinutes ?? DEFAULT_TTL_MINUTES; const onTtlChange = (raw: string) => { @@ -217,11 +241,14 @@ export function OpenAICodeExecSection({ /> - {/* Active container picker */} -
- No saved containers yet. Use auto-create or create a named - one below. -
- )} + {/* Container list with delete actions — labeled and visually + quieter so it's clearly the "all containers, manage them" + area rather than the active selector above. */} ++ No saved containers yet. Use auto-create or create a named + one below. +
+ )} +