eleven / ocho Public

home: creating a space asks nothing β€” the slider IS the create (#1446)

The create flow asked "What's your space called?" on the way in, and then
Eleven asked the same question inside the space, first thing. Two asks for
one answer, the first of them a form field between wanting a space and
having one.

Drop it: openCreate goes straight to the slider, the space is born
nameless, and Eleven names it in conversation. Home learns the name when
the space's key is added to the account (the enrol entry carries it).
Matches the native create, which never had a naming step.

The plan screen survives for the paid bounce alone, behind openPlanBounce.


Claude-Session: https://claude.ai/code/session_01GUA1EJFYwgr2nZNVBRFf8k
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell pushed by codex agent, operated by paul@keymail.dev 69352ab59b634c2c7d27be9ca84f912024a2bcbd parent 5d6cc29
3 files changed, +62 −60
  • changelog/2026-08-28-create-a-space-asks-nothing.md +4 −0
  • home/web/pages/index.html +11 −16
  • home/web/static/home.js +47 −44
diff --git a/changelog/2026-08-28-create-a-space-asks-nothing.md b/changelog/2026-08-28-create-a-space-asks-nothing.md
new file mode 100644
index 0000000..26a3b18
--- /dev/null
+++ b/changelog/2026-08-28-create-a-space-asks-nothing.md
@@ -0,0 +1,4 @@
+πŸ€– Creating a space no longer asks you to name it up front. Pull the slider to
+eleven and the space exists β€” then Eleven asks what to call it inside, along
+with everything else about setting it up. One question, in one place, instead
+of the same question twice.
diff --git a/home/web/pages/index.html b/home/web/pages/index.html
index 3c4b9c8..dd3d303 100644
--- a/home/web/pages/index.html
+++ b/home/web/pages/index.html
@@ -536,14 +536,10 @@
font-size: .62rem; font-weight: 600; color: var(--muted);
padding: 1px 8px; border: 1px solid var(--border); border-radius: 999px;
}
- /* Plan screen (create step 1): the name field + who-pays picker. Selection
- mirrors the band chips β€” border + weight, never colour alone. */
+ /* Plan screen: the who-pays picker, shown only when a create bounces off a
+ gate paying fixes. Selection mirrors the band chips β€” border + weight,
+ never colour alone. */
#create-plan { text-align: center; }
- #plan-name {
- width: 100%; margin-top: 8px; padding: 12px 14px; font: inherit; text-align: center;
- color: var(--ink); background: var(--quiet); border: 1px solid var(--border); border-radius: 12px;
- }
- #plan-name:focus-visible { outline: none; border-color: var(--accent); background: var(--panel); }
#plan-tiers-wrap { margin-top: 16px; }
#plan-tiers { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }
.tier-btn.selected { border-color: var(--accent); border-width: 2px; padding: 11px 13px; font-weight: 600; }
@@ -1071,24 +1067,23 @@
<p class="status" id="combine-status"></p>
</div>
- <!-- Name a space (create, step 1 of 2). The slider screen (step 2) then
- creates the space immediately β€” always free, never pay-gated: the plan
- question is Eleven's, inside the space (#1374). The tier cards below
- appear only when a create bounces off a gate paying genuinely fixes
- (the free quota, or the pool under budget pressure); picking one hands
- off to Stripe and the space is created the moment it's paid. -->
+ <!-- The plan screen: NOT part of the happy path. Creating a space is the
+ slider and nothing else β€” no name asked (Eleven asks inside the space,
+ where the answer is a message in a conversation, not a form field), no
+ plan asked (always free at birth, #1374). This screen appears only when
+ a create bounces off a gate paying genuinely fixes (the free quota, or
+ the pool under budget pressure); picking a tier hands off to Stripe and
+ the space is created the moment it's paid. -->
<div id="create-plan" class="hidden screen">
<button id="plan-back" class="icon-back" type="button" aria-label="Back to your spaces"><svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M10 3 5 8l5 5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
<h2 class="create-title">Create a space</h2>
- <label class="create-sub" for="plan-name">What's your space called?</label>
- <input id="plan-name" type="text" maxlength="60" autocomplete="off" placeholder="e.g. The Campbells">
<div id="plan-tiers-wrap" class="hidden">
<p class="create-sub" id="plan-tiers-lead">Pick a plan to create it now:</p>
<div id="plan-tiers" role="group" aria-label="Pick a plan"></div>
<p class="fx-note hidden" id="plan-fx"></p>
</div>
<div class="actions">
- <button id="plan-continue" class="secondary" type="button" disabled>Continue</button>
+ <button id="plan-continue" class="secondary" type="button">Continue</button>
</div>
<p class="status" id="plan-status"></p>
</div>
diff --git a/home/web/static/home.js b/home/web/static/home.js
index 4d6f932..d04e998 100644
--- a/home/web/static/home.js
+++ b/home/web/static/home.js
@@ -3314,8 +3314,11 @@ async function requestSpace() {
// and grind it out right here, during the fader's build phase β€” the CPU
// burn IS the "Hold on…" moment. bits 0 = no gate, nothing to solve.
const proof = await solveCreateChallenge();
+ // No name: a space is born nameless and Eleven asks inside it (the setup
+ // conversation's "group" step), where the answer is a reply in a
+ // conversation instead of a form field on the way in. Home learns the name
+ // when the space's key is added to this account (the enrol payload's entry).
const body = { ...(proof || {}) };
- if (planChoice && planChoice.name) body.name = planChoice.name;
const res = await fetch("/api/spaces", {
method: "POST",
headers: { Authorization: "Bearer " + state.token, "Content-Type": "application/json" },
@@ -3355,13 +3358,13 @@ async function requestPaidSpace() {
const res = await fetch("/api/spaces/checkout-create", {
method: "POST",
headers: { Authorization: "Bearer " + state.token, "Content-Type": "application/json" },
- body: JSON.stringify({ ...(proof || {}), name: planChoice.name, tier: planChoice.tier }),
+ body: JSON.stringify({ ...(proof || {}), tier: planChoice.tier }),
});
const reply = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(reply.message || reply.error || `Couldn't start the checkout (HTTP ${res.status}).`);
if (!reply.url) throw new Error("The checkout link didn't come back β€” try again.");
if (reply.session_id) {
- rememberPendingCreate({ session: reply.session_id, name: planChoice.name, at: Date.now() });
+ rememberPendingCreate({ session: reply.session_id, at: Date.now() });
}
location.href = reply.url;
return new Promise(() => {});
@@ -3369,7 +3372,7 @@ async function requestPaidSpace() {
// ---- the pay-first marker + claim ---------------------------------------------
-// The localStorage marker for a checkout in flight: {session, name, at}. It
+// The localStorage marker for a checkout in flight: {session, at}. It
// exists so a tab closed on Stripe's page still finishes the create on the
// next visit here β€” the claim is idempotent, so over-firing is safe.
const PENDING_CREATE_KEY = "lchat-pending-create";
@@ -3589,13 +3592,7 @@ function ensureFader() {
// showing (#1374). Everything else stays a plain inline error.
const code = (err && err.code) || "";
if ((code === "quota_exceeded" || code === "free_pool_full") && billingTiers.length) {
- fader?.stop();
- $("create").classList.add("hidden");
- planPaidRequired = true;
- planTier = billingTiers[0].key;
- renderPlanTiers();
- $("create-plan").classList.remove("hidden");
- planStatus((err && err.message) || "", "error");
+ openPlanBounce((err && err.message) || "");
return;
}
$("create-back").classList.remove("hidden");
@@ -3605,8 +3602,9 @@ function ensureFader() {
return fader;
}
-// The plan chosen on step 1 β€” consumed by the fader's onEleven to pick the
-// free create or the Stripe handoff. tier "" = free.
+// The plan behind the slider β€” consumed by the fader's onEleven to pick the
+// free create or the Stripe handoff. tier "" = free, which is every create
+// that didn't bounce off a paid gate.
let planChoice = null;
let planTier = "free";
// planPaidRequired: the create bounced off a gate paying genuinely fixes
@@ -3616,23 +3614,22 @@ let planTier = "free";
// space for?" is Eleven's question inside the space (#1374).
let planPaidRequired = false;
-// openCreate opens step 1: name the space. The slider (step 2) stays the
-// actual commit.
+// openCreate opens the create: the slider, and nothing before it. Nothing is
+// asked on the way in β€” not a name (Eleven asks inside the space), not a plan
+// (every space is born free, #1374) β€” so the fastest path from "I want a
+// space" to having one is a single gesture. The plan screen exists only for
+// the bounce (openPlanBounce).
function openCreate() {
clearUndo();
planChoice = null;
planTier = "free";
planPaidRequired = false;
- $("plan-name").value = "";
- $("plan-continue").disabled = true;
planStatus("");
- // The ledger read still warms the price book β€” the paid-required error
- // path needs it in hand (renderPlanTiers fills in when it answers).
+ // The ledger read warms the price book β€” the paid-required error path needs
+ // it in hand (renderPlanTiers fills in when it answers).
if (spacesEnabled && provisioned === null && state.token) refreshProvisioned();
- renderPlanTiers();
hideVault();
- $("create-plan").classList.remove("hidden");
- $("plan-name").focus();
+ openCreateFader();
}
function closePlan() {
@@ -3640,6 +3637,20 @@ function closePlan() {
showVault();
}
+// openPlanBounce shows the plan screen β€” the ONLY thing that does. The create
+// hit a gate paying genuinely fixes (the free quota, the pool under budget
+// pressure), so the paid tiers are the way through, with the gate's own
+// message above them.
+function openPlanBounce(message) {
+ fader?.stop();
+ $("create").classList.add("hidden");
+ planPaidRequired = true;
+ planTier = billingTiers[0].key;
+ renderPlanTiers();
+ $("create-plan").classList.remove("hidden");
+ planStatus(message, "error");
+}
+
function planStatus(msg, kind) {
const el = $("plan-status");
el.textContent = msg || "";
@@ -3647,8 +3658,8 @@ function planStatus(msg, kind) {
}
// renderPlanTiers paints the who-pays picker from the provisioner's price
-// book β€” but ONLY in paid-required mode (#1374): the happy path is just the
-// name, and the plan question lives in Eleven's setup conversation. Here the
+// book β€” but ONLY in paid-required mode (#1374): the happy path asks nothing
+// at all, and the plan question lives in Eleven's setup conversation. Here the
// create bounced off the quota or the pool gate, so the paid tiers (no Free
// card β€” free is exactly what bounced) are the way through, at base price
// with the β‰ˆ local figure the upgrade picker also shows.
@@ -3713,16 +3724,10 @@ function renderPlanTiers() {
fxNote.textContent = rates && fxSnapshot.date ? "Billed in euros β€” β‰ˆ at the ECB rate of " + fxSnapshot.date + "." : "";
}
-// planContinue commits step 1 and opens the slider. The name is required β€”
-// asking and then accepting silence would make the field noise.
+// planContinue commits the bounce's chosen plan and returns to the slider β€”
+// the same commit as the free path, with a Stripe handoff behind it.
function planContinue() {
- const name = $("plan-name").value.trim();
- if (!name) {
- planStatus("Give your space a name first.", "error");
- $("plan-name").focus();
- return;
- }
- planChoice = { name, tier: planTier === "free" ? "" : planTier };
+ planChoice = { tier: planTier === "free" ? "" : planTier };
$("create-plan").classList.add("hidden");
openCreateFader();
}
@@ -4326,18 +4331,16 @@ $("combine-skip-no").addEventListener("click", combineVaultStage);
$("open-create").addEventListener("click", openCreate);
$("plan-back").addEventListener("click", closePlan);
$("plan-continue").addEventListener("click", planContinue);
-$("plan-name").addEventListener("input", () => {
- $("plan-continue").disabled = !$("plan-name").value.trim();
- planStatus("");
-});
-$("plan-name").addEventListener("keydown", (e) => {
- if (e.key === "Enter" && !$("plan-continue").disabled) planContinue();
-});
-// Step 2's back returns to step 1 (the choices survive), not the list.
+// Back from the slider: the list normally (it's the only step there was), or
+// the plan screen when a bounce put one behind us β€” the chosen tier survives.
$("create-back").addEventListener("click", () => {
- fader?.stop();
- $("create").classList.add("hidden");
- $("create-plan").classList.remove("hidden");
+ if (planPaidRequired) {
+ fader?.stop();
+ $("create").classList.add("hidden");
+ $("create-plan").classList.remove("hidden");
+ return;
+ }
+ closeCreate();
});
$("create-done").addEventListener("click", closeCreate);
// (Creating a space is the fader now β€” mounted in openCreate; see fader.js.)