Create chat context for browser launches

Allow the Browser surface to create and select a chat context when opened without an active context. Reuse an in-flight context creation promise so repeated startup paths do not race, and update commands/viewer connection to ensure a context before calling browser websocket APIs. Add a browser regression guard for the no-context startup path.

Alessandro committed Apr 27, 2026 at 17:44 UTC ad76578c475e64cb5f05a81f64b3a8b150a6f495
2 files changed +68 -3
plugins/_browser/webui/browser-store.js
+55 -3
@@ -96,6 +96,7 @@ const model = {
96 _surfaceOpenedAt: 0,
97 _connectSequence: 0,
98 _viewerToken: "",
99 + _contextCreatePromise: null,
100 extensionMenuOpen: false,
101 extensionInstallUrl: "",
102 extensionActionLoading: false,
@@ -204,6 +205,47 @@ const model = {
205 return globalThis.getContext?.() || urlContext || selectedChat || "";
206 },
207
208 + async ensureContextId() {
209 + const existingContextId = String(this.resolveContextId() || "").trim();
210 + if (existingContextId) {
211 + this.contextId = existingContextId;
212 + return existingContextId;
213 + }
214 +
215 + if (!this._contextCreatePromise) {
216 + this._contextCreatePromise = this.createChatContextForBrowser();
217 + }
218 +
219 + try {
220 + this.contextId = await this._contextCreatePromise;
221 + return this.contextId;
222 + } finally {
223 + this._contextCreatePromise = null;
224 + }
225 + },
226 +
227 + async createChatContextForBrowser() {
228 + const response = await callJsonApi("/chat_create", {
229 + current_context: this.resolveContextId() || "",
230 + });
231 + const selectedContextId = String(this.resolveContextId() || "").trim();
232 + if (selectedContextId) return selectedContextId;
233 +
234 + const contextId = String(response?.ctxid || "").trim();
235 + if (!response?.ok || !contextId) {
236 + throw new Error(response?.error || "Could not create a chat for Browser.");
237 + }
238 +
239 + if (typeof globalThis.setContext === "function") {
240 + globalThis.setContext(contextId);
241 + } else {
242 + const chatsStore = globalThis.Alpine?.store?.("chats");
243 + chatsStore?.setSelected?.(contextId);
244 + }
245 +
246 + return contextId;
247 + },
248 +
249 async openExtensionsSettings() {
250 if (!pluginSettingsStore?.openConfig) {
251 this.error = "Browser settings are unavailable.";
@@ -386,8 +428,8 @@ const model = {
428 } else {
429 this.setupCanvasSurface(element);
430 }
389 - this.contextId = this.resolveContextId();
431 try {
432 + await this.ensureContextId();
433 await this.refreshStatus();
434 const viewport = await this.waitForSurfaceViewport();
435 this.resetRenderedFrameIfViewportChanged(viewport, requestedBrowserId);
@@ -475,9 +517,18 @@ const model = {
517 },
518
519 async connectViewer(options = {}) {
478 - if (!this.contextId) {
520 + let contextId = "";
521 + try {
522 + contextId = await this.ensureContextId();
523 + } catch (error) {
524 + this.connected = false;
525 + this.switchingBrowserId = null;
526 + this._surfaceSwitching = false;
527 + throw error;
528 + }
529 + if (!contextId) {
530 this.connected = false;
480 - this.error = "No active chat context is selected.";
531 + this.error = "Could not create a chat for Browser.";
532 this.switchingBrowserId = null;
533 this._surfaceSwitching = false;
534 return;
@@ -638,6 +689,7 @@ const model = {
689 this.commandInFlight = true;
690 const previousActiveBrowserId = this.activeBrowserId;
691 try {
692 + await this.ensureContextId();
693 const response = await websocket.request(
694 "browser_viewer_command",
695 {
tests/test_browser_agent_regressions.py
+13
@@ -347,6 +347,19 @@ def test_browser_viewer_allows_slow_extension_startup():
347 assert "Installing Chromium for the first Browser run" in js
348
349
350 +def test_browser_viewer_creates_chat_when_no_context_is_selected():
351 + js = (PROJECT_ROOT / "plugins" / "_browser" / "webui" / "browser-store.js").read_text(
352 + encoding="utf-8"
353 + )
354 +
355 + assert "async ensureContextId()" in js
356 + assert "async createChatContextForBrowser()" in js
357 + assert 'callJsonApi("/chat_create"' in js
358 + assert "globalThis.setContext(contextId)" in js
359 + assert "await this.ensureContextId();" in js
360 + assert "No active chat context is selected." not in js
361 +
362 +
363 def test_browser_canvas_startup_waits_for_raw_viewport_settle():
364 js = (PROJECT_ROOT / "plugins" / "_browser" / "webui" / "browser-store.js").read_text(
365 encoding="utf-8"