studio/chat: let user pick OpenAI container before first send

Previously the picker silently no-op'd until the user sent the first
message, because Dexie's ThreadRecord is only materialized inside the
runtime-provider's `initialize` hook (assistant-ui's first-message
callback). That kept users from binding a thread to an existing
OpenAI container up front; they had to either send a message and
risk the chat adapter auto-creating one, or accept the cross-thread
inheritance default.

- Export `ensureThreadRecord` from runtime-provider so other surfaces
  can materialize the row idempotently.
- In OpenAICodeExecSection.onPick, await ensureThreadRecord before
  the update, with modelType="base" (the settings sheet that hosts
  this section is only rendered in single-thread mode).

Behaviour after this commit:
- New thread + user picks a container in the sidebar → thread row is
  created with that container_id; first send uses it, no auto-create.
- New thread + user does nothing → row still absent; first send goes
  through the existing inherit/lazy-create path as before.
- The auto-bind effect remains silent best-effort: it does not
  eagerly create the thread row, so it cannot pre-empt the user's
  pick on a fresh thread.
This commit is contained in:
Roland Tannous 2026-05-15 23:04:03 +04:00
commit ef2b59bd9f
2 changed files with 14 additions and 10 deletions

View file

@ -44,6 +44,7 @@ import {
import { db } from "../db";
import type { ExternalProviderConfig } from "../external-providers";
import { useLiveQuery } from "../db";
import { ensureThreadRecord } from "../runtime-provider";
const AUTO_OPTION_VALUE = "__auto__";
const DEFAULT_TTL_MINUTES = 20;
@ -155,20 +156,23 @@ export function OpenAICodeExecSection({
const onPick = async (value: string) => {
if (!activeThreadId) return;
const next = value === AUTO_OPTION_VALUE ? null : value;
// 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.
// The thread row is normally materialized on first send by the
// chat adapter. When the user picks a container before sending the
// first message, the row does not exist yet and Dexie's `update`
// returns 0 rows — the selection would silently no-op and the
// picker would snap back to "Auto-create per thread". Eagerly
// create the row so the user can pin a container up front.
// modelType "base" is correct here: the settings sheet that hosts
// this section is only rendered in single-thread mode, where the
// chat-page passes modelType="base" into ChatRuntimeProvider.
try {
await ensureThreadRecord({ threadId: activeThreadId, modelType: "base" });
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.",
);
// Defensive: ensureThreadRecord should have written the row.
toast.error("Could not update thread.");
}
} catch (err) {
toast.error(

View file

@ -396,7 +396,7 @@ function toThreadMessage(m: MessageRecord): ThreadMessage {
};
}
async function ensureThreadRecord({
export async function ensureThreadRecord({
threadId,
modelType,
pairId,