From c2630598872764a125469e83f0e02caed997f153 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Fri, 15 May 2026 22:03:01 +0400 Subject: [PATCH] studio/chat: inherit container across threads + styled active picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New threads on the same OpenAI provider now default to the most recently used container instead of "Auto-create per thread" — both in the chat-adapter (so a send works even if the side panel was never opened) and in the side panel itself (auto-binds the active thread when the dropdown loads on a thread that has no container). Picker is visually emphasized with an accent panel and the currently-active row in the list below is highlighted with the same accent so the two views stay in sync. --- .../src/features/chat/api/chat-adapter.ts | 45 +++++- .../components/openai-code-exec-section.tsx | 130 ++++++++++++------ 2 files changed, 128 insertions(+), 47 deletions(-) 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 */} -
+ {/* Active container picker — visually emphasized so it reads as + the primary control vs. the static list below. Accent + background + ring outline distinguish it from the plain + bordered list items beneath. */} +
- - Active container (this thread) + + Active for this thread
- {/* Container list with delete actions */} - {isLoading && containers.length === 0 ? ( - - ) : containers.length > 0 ? ( -
    - {containers.map((c) => ( -
  • -
    - - {c.name ?? "(unnamed)"} - - - {c.id} · TTL{" "} - {c.expiresAfterMinutes ?? DEFAULT_TTL_MINUTES}m - -
    - -
  • - ))} -
- ) : ( -

- 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. */} +
+ + All containers + + {isLoading && containers.length === 0 ? ( + + ) : containers.length > 0 ? ( +
    + {containers.map((c) => { + const isActive = c.id === activeContainerId; + return ( +
  • +
    + + {c.name ?? "(unnamed)"} + {isActive ? ( + + · active + + ) : null} + + + {c.id} · TTL{" "} + {c.expiresAfterMinutes ?? DEFAULT_TTL_MINUTES}m + +
    + +
  • + ); + })} +
+ ) : ( +

+ No saved containers yet. Use auto-create or create a named + one below. +

+ )} +
{/* Create new */} {createOpen ? (