Session creation controls
+This harmless fixture mounts Clay's production desktop and mobile render functions with a mocked WebSocket. It starts no provider or daemon.
+diff --git a/lib/public/css/mobile-nav.css b/lib/public/css/mobile-nav.css
index 2a40c447..048d6e71 100644
--- a/lib/public/css/mobile-nav.css
+++ b/lib/public/css/mobile-nav.css
@@ -358,8 +358,7 @@
background: rgba(var(--overlay-rgb), 0.06);
}
- /* Split "New session" row: main action on the last-used vendor + a chevron
- that expands .mobile-vendor-list below it. Mirrors the desktop sidebar. */
+ /* Primary creation action + a labeled provider selector. */
.mobile-session-new-row {
display: flex;
gap: 6px;
@@ -377,26 +376,30 @@
color: var(--text);
gap: 8px;
}
- .mobile-session-new-row .mobile-session-new-chevron {
- flex: 0 0 auto;
- width: 44px;
+ .mobile-session-new-row .mobile-session-new-provider {
+ flex: 0 1 132px;
+ width: 132px;
+ min-width: 104px;
margin-bottom: 0;
- padding: 10px 0;
+ padding: 6px 9px;
border: 1px solid var(--border-subtle);
border-radius: 8px;
background: rgba(var(--overlay-rgb), 0.03);
color: var(--text-muted);
- justify-content: center;
- gap: 0;
+ display: grid;
+ gap: 2px;
}
- .mobile-session-new-row .mobile-session-new-chevron.expanded svg {
+ .mobile-session-new-row .mobile-session-new-provider.expanded .session-create-provider-chevron svg {
transform: rotate(180deg);
}
.mobile-session-new-row .mobile-session-new-main:active,
- .mobile-session-new-row .mobile-session-new-chevron:active {
+ .mobile-session-new-row .mobile-session-new-provider:active {
background: rgba(var(--overlay-rgb), 0.1);
}
+ .mobile-session-new-row .session-create-provider-label { font-size: 9px; }
+ .mobile-session-new-row .session-create-provider-name { font-size: 12px; }
+
.mobile-vendor-list {
display: flex;
flex-direction: column;
diff --git a/lib/public/css/sidebar.css b/lib/public/css/sidebar.css
index 66299140..3d8361f7 100644
--- a/lib/public/css/sidebar.css
+++ b/lib/public/css/sidebar.css
@@ -1262,26 +1262,77 @@
opacity: 1;
}
-/* Split button: main action + chevron sharing one grid cell. */
+/* Primary creation action + a visibly grouped provider selector. */
.session-top-action-split {
display: flex;
+ flex-wrap: wrap;
align-items: stretch;
- gap: 2px;
+ gap: 6px;
min-width: 0;
}
.session-top-action-split .split-main {
- flex: 1 1 auto;
+ flex: 1 1 110px;
width: auto;
+ min-width: 110px;
+ border: 1px solid color-mix(in srgb, var(--accent) 34%, transparent);
+ background: color-mix(in srgb, var(--accent) 13%, transparent);
+ color: var(--text);
+ font-weight: 600;
+ white-space: nowrap;
}
-.session-top-action-split .split-chevron {
- flex: 0 0 auto;
- width: 28px;
- min-width: 28px;
- padding: 0;
- justify-content: center;
- gap: 0;
+.session-top-action-split .split-main:hover {
+ border-color: color-mix(in srgb, var(--accent) 50%, transparent);
+ background: color-mix(in srgb, var(--accent) 19%, transparent);
+}
+
+.session-top-action-split .split-provider {
+ flex: 1 1 104px;
+ width: auto;
+ min-width: 104px;
+ padding: 5px 8px;
+ border: 1px solid var(--border-subtle);
+ background: color-mix(in srgb, var(--sidebar-hover) 48%, transparent);
+ border-radius: 9px;
+ display: grid;
+ gap: 1px;
+}
+
+.session-create-provider-label {
+ display: block;
+ color: var(--text-dimmer);
+ font-size: 9px;
+ font-weight: 600;
+ line-height: 1;
+ letter-spacing: 0.03em;
+ text-transform: uppercase;
+}
+
+.session-create-provider-choice {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ min-width: 0;
+}
+
+.session-create-provider-name {
+ overflow: hidden;
+ color: var(--text-secondary);
+ font-size: 11px;
+ font-weight: 600;
+ line-height: 1.2;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.session-create-provider-chevron {
+ display: inline-flex;
+ margin-left: auto;
+}
+
+.session-create-provider[aria-expanded="true"] .session-create-provider-chevron svg {
+ transform: rotate(180deg);
}
.session-favorites-section {
diff --git a/lib/public/modules/session-creation-cta.js b/lib/public/modules/session-creation-cta.js
new file mode 100644
index 00000000..17fcf5c3
--- /dev/null
+++ b/lib/public/modules/session-creation-cta.js
@@ -0,0 +1,33 @@
+// Shared, presentation-only session creation controls for desktop and mobile.
+
+import { iconHtml } from './icons.js';
+
+export function renderSessionCreationCta(options) {
+ var row = document.createElement("div");
+ row.className = options.rowClass + " session-create-cta";
+
+ var createButton = document.createElement("button");
+ createButton.className = options.createClass + " session-create-primary";
+ createButton.type = "button";
+ createButton.textContent = "Create session";
+ createButton.title = options.createTitle;
+ createButton.setAttribute("aria-label", options.createTitle);
+ createButton.addEventListener("click", options.onCreate);
+ row.appendChild(createButton);
+
+ var providerButton = document.createElement("button");
+ providerButton.className = options.providerClass + " session-create-provider";
+ providerButton.type = "button";
+ providerButton.title = options.providerTitle;
+ providerButton.setAttribute("aria-label", "AI provider: " + options.vendorName + ". " + options.providerTitle);
+ providerButton.setAttribute("aria-expanded", "false");
+ providerButton.setAttribute("aria-controls", options.menuId);
+ providerButton.innerHTML = 'AI provider' +
+ '' +
+ options.vendorName + '' + iconHtml("chevron-down", "session-create-provider-chevron") + '';
+ providerButton.addEventListener("click", options.onProvider);
+ row.appendChild(providerButton);
+
+ return { element: row, createButton: createButton, providerButton: providerButton };
+}
diff --git a/lib/public/modules/sidebar-mobile.js b/lib/public/modules/sidebar-mobile.js
index a0d9068a..18d3377a 100644
--- a/lib/public/modules/sidebar-mobile.js
+++ b/lib/public/modules/sidebar-mobile.js
@@ -16,6 +16,7 @@ import {
} from './sidebar-sessions.js';
import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES, isExperimentalVendor } from './app-rendering.js';
import { defaultVendorUnavailable, resolvePreferredVendor, saveDefaultVendor } from './default-vendor.js';
+import { renderSessionCreationCta } from './session-creation-cta.js';
import {
getCachedProjectList,
getCachedCurrentSlug,
@@ -671,42 +672,49 @@ function createMobileLoopGroup(loopId, children, groupKey) {
}
// Helper: render sorted sessions into a container with date groups (with loop session grouping)
-function renderMobileSessionsInto(container) {
- // Mirrors the desktop sidebar's split button: one "New session" action on
- // the project's last-used vendor, plus a chevron that expands the full
- // vendor list. The list expands inline rather than as a fixed-position
- // popover because it lives inside a scrolling bottom sheet.
+export function renderMobileSessionsInto(container) {
var mobileDefaultVendor = resolvePreferredVendor(store.get('installedVendors') || []) || resolveDefaultVendor();
var mobileInstalled = store.get('installedVendors') || [];
var mobileDefaultState = store.get('defaultVendorState') || {};
- var newRow = document.createElement("div");
- newRow.className = "mobile-session-new-row";
-
- var mainBtn = document.createElement("button");
- mainBtn.className = "mobile-session-new mobile-session-new-main";
- mainBtn.title = "New " + (VENDOR_NAMES[mobileDefaultVendor] || mobileDefaultVendor) + " session";
- if (defaultVendorUnavailable(mobileInstalled)) mainBtn.title += " (saved default is unavailable; using installed priority)";
- mainBtn.innerHTML = '
New ' + (VENDOR_NAMES[mobileDefaultVendor] || mobileDefaultVendor) + ' session';
- mainBtn.addEventListener("click", function () {
- startNewSession(mobileDefaultVendor);
- closeMobileSheet();
- });
- newRow.appendChild(mainBtn);
-
var vendorList = document.createElement("div");
vendorList.className = "mobile-vendor-list hidden";
-
- var chevronBtn = document.createElement("button");
- chevronBtn.className = "mobile-session-new mobile-session-new-chevron";
- chevronBtn.setAttribute("aria-label", "Choose a vendor");
- chevronBtn.innerHTML = iconHtml("chevron-down");
- chevronBtn.addEventListener("click", function () {
- vendorList.classList.toggle("hidden");
- chevronBtn.classList.toggle("expanded");
+ vendorList.id = "mobile-session-provider-menu";
+ var mobileVendorName = VENDOR_NAMES[mobileDefaultVendor] || mobileDefaultVendor;
+ var mobileCreateTitle = "Create a " + mobileVendorName + " session";
+ if (defaultVendorUnavailable(mobileInstalled)) mobileCreateTitle += " (saved default is unavailable; using installed priority)";
+ var controls;
+ controls = renderSessionCreationCta({
+ rowClass: "mobile-session-new-row",
+ createClass: "mobile-session-new mobile-session-new-main",
+ providerClass: "mobile-session-new mobile-session-new-provider",
+ iconClass: "mobile-session-new-icon",
+ vendorName: mobileVendorName,
+ vendorAvatar: VENDOR_AVATARS[mobileDefaultVendor] || VENDOR_AVATARS.claude,
+ createTitle: mobileCreateTitle,
+ providerTitle: "Choose an AI provider",
+ menuId: vendorList.id,
+ onCreate: function () { startNewSession(mobileDefaultVendor); closeMobileSheet(); },
+ onProvider: function () {
+ var opening = vendorList.classList.contains("hidden");
+ vendorList.classList.toggle("hidden", !opening);
+ controls.providerButton.classList.toggle("expanded", opening);
+ controls.providerButton.setAttribute("aria-expanded", opening ? "true" : "false");
+ if (opening) {
+ var first = vendorList.querySelector("button:not(:disabled)");
+ if (first) first.focus();
+ }
+ },
+ });
+ var newRow = controls.element;
+ vendorList.addEventListener("keydown", function (e) {
+ if (e.key !== "Escape") return;
+ e.preventDefault();
+ vendorList.classList.add("hidden");
+ controls.providerButton.classList.remove("expanded");
+ controls.providerButton.setAttribute("aria-expanded", "false");
+ controls.providerButton.focus();
});
- newRow.appendChild(chevronBtn);
if (defaultVendorUnavailable(mobileInstalled) && mobileDefaultState.preference) {
var mobileFallbackNote = document.createElement("div");
@@ -727,7 +735,7 @@ function renderMobileSessionsInto(container) {
if (!isInstalled) vBtn.classList.add("disabled");
if (vendor === mobileDefaultVendor) vBtn.classList.add("active");
vBtn.innerHTML = '
' +
- '' + name + '' +
+ '' + (isInstalled ? "Create with " : "Learn about ") + name + '' +
(isExperimentalVendor(vendor) ? '🧪' : '') +
(isInstalled ? "" : 'Not installed');
vBtn.addEventListener("click", function () {
@@ -735,6 +743,9 @@ function renderMobileSessionsInto(container) {
window.open(VENDOR_HOMEPAGES[vendor], "_blank", "noopener");
return;
}
+ vendorList.classList.add("hidden");
+ controls.providerButton.classList.remove("expanded");
+ controls.providerButton.setAttribute("aria-expanded", "false");
startNewSession(vendor);
closeMobileSheet();
});
diff --git a/lib/public/modules/sidebar-sessions.js b/lib/public/modules/sidebar-sessions.js
index ddce0aeb..b63bf818 100644
--- a/lib/public/modules/sidebar-sessions.js
+++ b/lib/public/modules/sidebar-sessions.js
@@ -17,6 +17,7 @@ import { openGroup, separateGroup } from './split-view.js';
import { groupedSessionIds } from './split-group-helpers.js';
import { defaultVendorUnavailable, resolvePreferredVendor, saveDefaultVendor } from './default-vendor.js';
import { openPairDialog } from './split-pair-ui.js';
+import { renderSessionCreationCta } from './session-creation-cta.js';
import {
hierarchyItemMatches,
prepareSidebarHierarchy,
@@ -53,10 +54,11 @@ var armedDeleteTimer = null;
store.subscribe(function (state, previous) {
if (state.defaultVendorState !== previous.defaultVendorState || state.installedVendors !== previous.installedVendors) {
var reopenNewMenu = !!(sessionCtxMenu && sessionCtxMenuAnchor);
+ var sessionList = getSessionListEl();
if (reopenNewMenu) closeSessionCtxMenu();
- if (getSessionListEl()) renderSessionList();
- if (reopenNewMenu) {
- var freshAnchor = getSessionListEl().querySelector(".session-top-action.split-chevron");
+ if (sessionList) renderSessionList();
+ if (reopenNewMenu && sessionList) {
+ var freshAnchor = sessionList.querySelector(".session-top-action.split-provider");
if (freshAnchor) showNewSessionMenu(freshAnchor, resolveDefaultVendor());
}
}
@@ -356,58 +358,49 @@ export function startNewSession(vendor, extra) {
});
}
-function renderSessionTopActions() {
+export function renderSessionTopActions() {
var wrap = document.createElement("div");
wrap.className = "session-top-actions";
- // Single split button. The main half creates a session with the project's
- // last-used vendor (for Claude the server applies the claudeOpenMode pref);
- // the chevron opens the vendor picker plus alternate launch modes.
var defaultVendor = resolveDefaultVendor();
-
- var cell = document.createElement("div");
- cell.className = "session-top-action-split";
-
- var mainBtn = document.createElement("button");
- mainBtn.className = "session-top-action split-main";
- mainBtn.type = "button";
- mainBtn.title = "New " + (VENDOR_NAMES[defaultVendor] || defaultVendor) + " session";
- if (defaultVendorUnavailable(store.get('installedVendors') || [])) mainBtn.title += " (saved default is unavailable; using installed priority)";
- mainBtn.innerHTML = '
New session';
- mainBtn.addEventListener("click", function () {
- startNewSession(defaultVendor);
- });
- cell.appendChild(mainBtn);
-
- var chevron = document.createElement("button");
- chevron.className = "session-top-action split-chevron";
- chevron.type = "button";
- chevron.title = "Choose a vendor or launch mode";
- chevron.setAttribute("aria-label", "Choose a vendor or launch mode");
- chevron.innerHTML = iconHtml("chevron-down");
- chevron.addEventListener("click", function (e) {
- e.preventDefault();
- e.stopPropagation();
- if (sessionCtxMenu) { closeSessionCtxMenu(); return; }
- showNewSessionMenu(chevron, defaultVendor);
+ var vendorName = VENDOR_NAMES[defaultVendor] || defaultVendor;
+ var createTitle = "Create a " + vendorName + " session";
+ if (defaultVendorUnavailable(store.get('installedVendors') || [])) createTitle += " (saved default is unavailable; using installed priority)";
+ var controls;
+ controls = renderSessionCreationCta({
+ rowClass: "session-top-action-split",
+ createClass: "session-top-action split-main",
+ providerClass: "session-top-action split-provider",
+ iconClass: "session-top-action-icon",
+ vendorName: vendorName,
+ vendorAvatar: VENDOR_AVATARS[defaultVendor] || VENDOR_AVATARS.claude,
+ createTitle: createTitle,
+ providerTitle: "Choose an AI provider or launch mode",
+ menuId: "session-new-menu",
+ onCreate: function () { startNewSession(defaultVendor); },
+ onProvider: function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ if (sessionCtxMenu) { closeSessionCtxMenu(); return; }
+ showNewSessionMenu(controls.providerButton, defaultVendor);
+ },
});
- cell.appendChild(chevron);
-
- wrap.appendChild(cell);
+ wrap.appendChild(controls.element);
return wrap;
}
-// Dropdown anchored to the split-button chevron. Reuses the session-ctx-menu
-// element/var so the global document click handler closes it.
+// Provider disclosure anchored to the grouped provider control. Reuses the
+// session-ctx-menu element/var so the global document click handler closes it.
function showNewSessionMenu(anchorBtn, defaultVendor) {
closeSessionCtxMenu();
var menu = document.createElement("div");
menu.className = "session-ctx-menu session-new-menu";
+ menu.id = "session-new-menu";
var installed = store.get('installedVendors') || [];
var defaultVendorState = store.get('defaultVendorState') || {};
sessionCtxMenuAnchor = anchorBtn;
+ anchorBtn.setAttribute("aria-expanded", "true");
if (defaultVendorUnavailable(installed) && defaultVendorState.preference) {
var fallbackNote = document.createElement("div");
@@ -430,7 +423,7 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
if (vendor === defaultVendor) item.classList.add("active");
item.type = "button";
item.innerHTML = '
' +
- '' + name + '' +
+ '' + (isInstalled ? "Create with " : "Learn about ") + name + '' +
(isExperimentalVendor(vendor) ? '🧪' : '') +
(isInstalled ? "" : 'Not installed' + iconHtml("external-link"));
item.title = isInstalled
@@ -526,6 +519,12 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
document.body.appendChild(menu);
sessionCtxMenu = menu;
+ menu.addEventListener("keydown", function (e) {
+ if (e.key !== "Escape") return;
+ e.preventDefault();
+ e.stopPropagation();
+ closeSessionCtxMenu(true);
+ });
refreshIcons();
requestAnimationFrame(function () {
@@ -542,6 +541,8 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
if (menuRect.bottom > window.innerHeight - 8) {
menu.style.top = (btnRect.top - menuRect.height - 2) + "px";
}
+ var firstMenuItem = menu.querySelector("button:not(:disabled)");
+ if (firstMenuItem) firstMenuItem.focus();
});
}
@@ -718,13 +719,16 @@ export function getExpandedLoopRuns() {
// --- Context menu ---
-function closeSessionCtxMenu() {
+function closeSessionCtxMenu(restoreFocus) {
+ var anchor = sessionCtxMenuAnchor;
+ if (anchor) anchor.setAttribute("aria-expanded", "false");
sessionCtxMenuAnchor = null;
if (sessionCtxMenu) {
sessionCtxMenu.remove();
sessionCtxMenu = null;
sessionCtxSessionId = null;
}
+ if (restoreFocus && anchor && document.contains(anchor)) anchor.focus();
}
function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
diff --git a/test/fixtures/session-creation-cta-browser-check.js b/test/fixtures/session-creation-cta-browser-check.js
new file mode 100644
index 00000000..26740f43
--- /dev/null
+++ b/test/fixtures/session-creation-cta-browser-check.js
@@ -0,0 +1,153 @@
+var assert = require("node:assert/strict");
+var childProcess = require("node:child_process");
+var fs = require("node:fs");
+var os = require("node:os");
+var path = require("node:path");
+
+var sessionName = "clay-cta-check-" + process.pid;
+var serverPath = path.join(__dirname, "session-creation-cta-server.js");
+var outputDir = process.argv[2] || path.join(os.tmpdir(), "clay-session-cta-browser");
+var server = null;
+
+function browser(args, capture) {
+ var command = ["agent-browser", "--session", sessionName].concat(args);
+ var output = childProcess.execFileSync("npx", command, {
+ cwd: path.join(__dirname, "../.."),
+ encoding: "utf8",
+ stdio: capture ? ["ignore", "pipe", "inherit"] : ["ignore", "ignore", "inherit"],
+ });
+ return typeof output === "string" ? output.trim() : "";
+}
+
+function evaluate(expression) {
+ var value = JSON.parse(browser(["eval", expression], true));
+ if (typeof value === "string" && (/^[\[{]/).test(value)) return JSON.parse(value);
+ return value;
+}
+
+function waitForServer(child) {
+ return new Promise(function (resolve, reject) {
+ var buffer = "";
+ function onData(chunk) {
+ buffer += chunk.toString();
+ var newline = buffer.indexOf("\n");
+ if (newline === -1) return;
+ child.stdout.off("data", onData);
+ resolve(buffer.slice(0, newline).trim());
+ }
+ child.stdout.on("data", onData);
+ child.once("error", reject);
+ child.once("exit", function (code) {
+ if (code !== null && code !== 0) reject(new Error("Fixture server exited with " + code));
+ });
+ });
+}
+
+function pngDimensions(file) {
+ var data = fs.readFileSync(file);
+ return { width: data.readUInt32BE(16), height: data.readUInt32BE(20) };
+}
+
+function openAt(url, width, height) {
+ browser(["set", "viewport", String(width), String(height)], false);
+ browser(["open", url], false);
+ browser(["wait", "500"], false);
+ assert.equal(evaluate("!!document.querySelector('.session-create-primary')"), true);
+ var viewport = evaluate("JSON.stringify({width:window.innerWidth,height:window.innerHeight})");
+ assert.deepEqual(viewport, { width: width, height: height });
+}
+
+async function main() {
+ fs.mkdirSync(outputDir, { recursive: true });
+ server = childProcess.spawn(process.execPath, [serverPath], { stdio: ["ignore", "pipe", "inherit"] });
+ var url = await waitForServer(server);
+ try {
+ openAt(url, 1280, 800);
+ var desktopMain = evaluate("window.fixtureMessages.splice(0);document.querySelector('.fixture-sidebar .session-create-primary').click();JSON.stringify(window.fixtureMessages)");
+ assert.deepEqual(desktopMain, [{ type: "new_session", vendor: "codex" }]);
+ evaluate("document.querySelector('.fixture-sidebar .session-create-provider').focus();document.querySelector('.fixture-sidebar .session-create-provider').click();true");
+ browser(["wait", "100"], false);
+ var desktopDisclosure = evaluate("JSON.stringify({messages:window.fixtureMessages,expanded:document.querySelector('.fixture-sidebar .session-create-provider').getAttribute('aria-expanded'),active:document.activeElement.textContent.trim()})");
+ assert.equal(desktopDisclosure.messages.length, 1);
+ assert.equal(desktopDisclosure.expanded, "true");
+ assert.match(desktopDisclosure.active, /^Create with Claude Code/);
+ browser(["press", "Escape"], false);
+ var desktopEscape = evaluate("JSON.stringify({expanded:document.querySelector('.fixture-sidebar .session-create-provider').getAttribute('aria-expanded'),focused:document.activeElement===document.querySelector('.fixture-sidebar .session-create-provider')})");
+ assert.deepEqual(desktopEscape, { expanded: "false", focused: true });
+ evaluate("document.querySelector('.fixture-sidebar .session-create-provider').click();document.querySelector('.session-new-vendor').click();true");
+ assert.deepEqual(evaluate("JSON.stringify(window.fixtureMessages)"), [
+ { type: "new_session", vendor: "codex" },
+ { type: "new_session", vendor: "claude" },
+ ]);
+
+ openAt(url, 1280, 800);
+ evaluate("document.querySelector('.fixture-sidebar .session-create-provider').click();document.querySelector('.session-new-set-default').click();true");
+ var defaultMessages = evaluate("JSON.stringify(window.fixtureMessages)");
+ assert.equal(defaultMessages.length, 1);
+ assert.equal(defaultMessages[0].type, "default_vendor_set");
+ assert.equal(defaultMessages[0].vendor, "claude");
+ var desktop240 = evaluate("JSON.stringify((function(){var side=document.querySelector('.fixture-sidebar');var row=side.querySelector('.session-create-cta');var main=side.querySelector('.session-create-primary');return {fits:side.scrollWidth<=side.clientWidth,rowFits:row.scrollWidth<=row.clientWidth,mainFits:main.scrollWidth<=main.clientWidth,whiteSpace:getComputedStyle(main).whiteSpace,mainHeight:Math.round(main.getBoundingClientRect().height)};})())");
+ assert.deepEqual(desktop240, { fits: true, rowFits: true, mainFits: true, whiteSpace: "nowrap", mainHeight: 34 });
+ var desktop192 = evaluate("document.querySelector('.fixture-page').style.gridTemplateColumns='192px minmax(0,1fr)';JSON.stringify((function(){var side=document.querySelector('.fixture-sidebar');var row=side.querySelector('.session-create-cta');var main=side.querySelector('.session-create-primary');var provider=side.querySelector('.session-create-provider');return {fits:side.scrollWidth<=side.clientWidth,rowFits:row.scrollWidth<=row.clientWidth,mainFits:main.scrollWidth<=main.clientWidth,stacked:provider.getBoundingClientRect().top>main.getBoundingClientRect().top,mainWidth:Math.round(main.getBoundingClientRect().width),providerWidth:Math.round(provider.getBoundingClientRect().width)};})())");
+ assert.equal(desktop192.fits, true);
+ assert.equal(desktop192.rowFits, true);
+ assert.equal(desktop192.mainFits, true);
+ assert.equal(desktop192.stacked, true);
+ assert.equal(desktop192.mainWidth, desktop192.providerWidth);
+ evaluate("document.querySelector('.fixture-page').style.gridTemplateColumns='240px minmax(0,1fr)';document.documentElement.classList.remove('light-theme');true");
+ var desktopDark = path.join(outputDir, "desktop-1280x800-dark.png");
+ browser(["screenshot", "body", desktopDark], false);
+ evaluate("document.documentElement.classList.add('light-theme');true");
+ var desktopLight = path.join(outputDir, "desktop-1280x800-light.png");
+ browser(["screenshot", "body", desktopLight], false);
+ assert.deepEqual(pngDimensions(desktopDark), { width: 1280, height: 800 });
+ assert.deepEqual(pngDimensions(desktopLight), { width: 1280, height: 800 });
+
+ openAt(url, 390, 844);
+ var mobileMain = evaluate("window.fixtureMessages.splice(0);document.querySelector('#mobile-fixture .session-create-primary').click();JSON.stringify(window.fixtureMessages)");
+ assert.deepEqual(mobileMain, [{ type: "new_session", vendor: "codex" }]);
+ evaluate("document.querySelector('#mobile-fixture .session-create-provider').focus();document.querySelector('#mobile-fixture .session-create-provider').click();true");
+ browser(["wait", "100"], false);
+ var mobileDisclosure = evaluate("JSON.stringify({messages:window.fixtureMessages,expanded:document.querySelector('#mobile-fixture .session-create-provider').getAttribute('aria-expanded'),fits:document.getElementById('mobile-actions').scrollWidth<=document.getElementById('mobile-actions').clientWidth})");
+ assert.equal(mobileDisclosure.messages.length, 1);
+ assert.equal(mobileDisclosure.expanded, "true");
+ assert.equal(mobileDisclosure.fits, true);
+ browser(["press", "Escape"], false);
+ var mobileEscape = evaluate("JSON.stringify({expanded:document.querySelector('#mobile-fixture .session-create-provider').getAttribute('aria-expanded'),focused:document.activeElement===document.querySelector('#mobile-fixture .session-create-provider')})");
+ assert.deepEqual(mobileEscape, { expanded: "false", focused: true });
+ evaluate("document.querySelector('#mobile-fixture .session-create-provider').click();document.querySelector('#mobile-session-provider-menu .mobile-session-new-vendor').click();true");
+ assert.deepEqual(evaluate("JSON.stringify(window.fixtureMessages)"), [
+ { type: "new_session", vendor: "codex" },
+ { type: "new_session", vendor: "claude" },
+ ]);
+ openAt(url, 390, 844);
+ evaluate("document.querySelector('#mobile-fixture .session-create-provider').click();document.querySelector('#mobile-session-provider-menu .mobile-vendor-set-default').click();true");
+ var mobileDefault = evaluate("JSON.stringify(window.fixtureMessages)");
+ assert.equal(mobileDefault.length, 1);
+ assert.equal(mobileDefault[0].type, "default_vendor_set");
+ assert.equal(mobileDefault[0].vendor, "claude");
+ evaluate("document.documentElement.classList.remove('light-theme');true");
+ var mobileDark = path.join(outputDir, "mobile-390x844-dark.png");
+ browser(["screenshot", "body", mobileDark], false);
+ evaluate("document.documentElement.classList.add('light-theme');true");
+ var mobileLight = path.join(outputDir, "mobile-390x844-light.png");
+ browser(["screenshot", "body", mobileLight], false);
+ assert.deepEqual(pngDimensions(mobileDark), { width: 390, height: 844 });
+ assert.deepEqual(pngDimensions(mobileLight), { width: 390, height: 844 });
+
+ process.stdout.write(JSON.stringify({
+ desktop: { viewport: "1280x800", minimumSidebar: 192, payloads: "passed", focus: "passed", overflow: "passed" },
+ mobile: { viewport: "390x844", payloads: "passed", focus: "passed", overflow: "passed" },
+ screenshots: [desktopDark, desktopLight, mobileDark, mobileLight],
+ }) + "\n");
+ } finally {
+ try { browser(["close"], false); } catch (error) {}
+ if (server && server.exitCode === null) server.kill("SIGTERM");
+ }
+}
+
+main().catch(function (error) {
+ process.stderr.write((error && error.stack) || String(error));
+ process.stderr.write("\n");
+ process.exitCode = 1;
+});
diff --git a/test/fixtures/session-creation-cta-server.js b/test/fixtures/session-creation-cta-server.js
new file mode 100644
index 00000000..5fb7e0b2
--- /dev/null
+++ b/test/fixtures/session-creation-cta-server.js
@@ -0,0 +1,36 @@
+var http = require("http");
+var fs = require("fs");
+var path = require("path");
+
+var root = path.join(__dirname, "../..");
+var publicRoot = path.join(root, "lib/public");
+
+function contentType(file) {
+ if (/\.css$/.test(file)) return "text/css";
+ if (/\.js$/.test(file)) return "text/javascript";
+ if (/\.html$/.test(file)) return "text/html";
+ if (/\.png$/.test(file)) return "image/png";
+ if (/\.svg$/.test(file)) return "image/svg+xml";
+ return "application/octet-stream";
+}
+
+var server = http.createServer(function (req, res) {
+ var requestPath = req.url.split("?")[0];
+ var file = requestPath === "/" ? path.join(__dirname, "session-creation-cta.html") : path.join(publicRoot, requestPath);
+ if (file.indexOf(publicRoot) !== 0 && file !== path.join(__dirname, "session-creation-cta.html")) {
+ res.writeHead(404); res.end("Not found"); return;
+ }
+ if (!fs.existsSync(file) || fs.statSync(file).isDirectory()) {
+ res.writeHead(404); res.end("Not found"); return;
+ }
+ res.writeHead(200, { "content-type": contentType(file), "cache-control": "no-store" });
+ fs.createReadStream(file).pipe(res);
+});
+
+server.listen(0, "127.0.0.1", function () {
+ process.stdout.write("http://127.0.0.1:" + server.address().port + "\n");
+});
+
+function close() { server.close(function () { process.exit(0); }); }
+process.on("SIGINT", close);
+process.on("SIGTERM", close);
diff --git a/test/fixtures/session-creation-cta.html b/test/fixtures/session-creation-cta.html
new file mode 100644
index 00000000..5ff6945d
--- /dev/null
+++ b/test/fixtures/session-creation-cta.html
@@ -0,0 +1,75 @@
+
+
+
This harmless fixture mounts Clay's production desktop and mobile render functions with a mocked WebSocket. It starts no provider or daemon.
+