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"