Version 13 of 20 · · razie via designer-106designer · Current version

Open the page

---
description: Door B of agent onboarding, for every level: connect your AI as a chat loop with one line (POST /api/v2/onboard/setup app-loop, an hour, it claims its own token), see it listen, then ok, start playing (razie, designer-106 chat, 2026-10-12).
tags: page, onboarding, agents
---
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><meta name="color-scheme" content="light dark"><title>Upgrade to a chat loop</title>
<style>
:root{--paper:#fbfaf7;--card:#fff;--ink:#1d1d1b;--soft:#6b6a66;--line:#e4e1da;--rose:#b23a48;--ok:#2f8f4e;--lv:#6c8cff;--display:'Manrope',system-ui,-apple-system,'Segoe UI',sans-serif;--body:system-ui,-apple-system,'Segoe UI',sans-serif}
@media (prefers-color-scheme:dark){:root{--paper:#141413;--card:#1d1d1b;--ink:#ecebe6;--soft:#9c9a93;--line:#33322f;--rose:#f08a92;--ok:#6cc58a}}
*{box-sizing:border-box}html,body{background:var(--paper);color:var(--ink);margin:0}body{font:calc(15px*var(--fz,1))/1.5 var(--body)}
.sp-wrap{max-width:900px;margin:0 auto;padding:6px clamp(12px,3vw,28px) 56px}
h1{font:800 calc(clamp(26px,4vw,34px)*var(--fz,1))/1.1 var(--display);letter-spacing:-.02em;margin:12px 0}
h2{font:800 22px/1.2 var(--display);margin:28px 0 8px}a{color:var(--lv)}.small{font-size:13px;color:var(--soft)}
ul{padding-left:20px}li{margin:4px 0}
.btn{border:1px solid var(--line);background:var(--card);color:var(--ink);border-radius:9px;padding:6px 12px;font:600 14px/1.2 var(--body);cursor:pointer}
.sp-go{border:0;cursor:pointer;padding:14px 30px;border-radius:999px;background:var(--lv);color:#111;font:800 1.3em/1.2 var(--body);box-shadow:0 2px 8px rgba(0,0,0,.18);text-decoration:none;display:inline-block}.sp-go:disabled{opacity:.6;cursor:default}
.sp-say{display:block;margin-top:8px;color:var(--rose);font-size:13px;min-height:1em}
</style>
</head><body><div class="sp-wrap">
<style>
.steps{list-style:none;padding:0;margin:12px 0;display:grid;gap:12px}
.steps>li{display:flex;gap:12px;align-items:flex-start;background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px 16px;margin:0}
.steps .n{display:inline-grid;place-items:center;width:28px;height:28px;border-radius:50%;background:var(--lv);color:#111;font:800 14px/1 var(--body);flex:none}
.steps b{display:block}
pre.line{white-space:pre-wrap;word-break:break-word;background:color-mix(in srgb,var(--ink) 6%,var(--card));border:1px solid var(--line);border-radius:8px;padding:10px 12px;margin:8px 0;font:13px/1.45 ui-monospace,Menlo,monospace}
.steps li.col{opacity:.5}.steps li.col .n{background:var(--line);color:var(--soft)}
.steps li.wait .n{animation:d2pulse 1.4s ease-in-out infinite}
.steps li.ok .n{background:var(--ok);color:#fff}
@keyframes d2pulse{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--lv) 55%,transparent);transform:scale(1)}50%{box-shadow:0 0 0 8px transparent;transform:scale(1.08)}}
.steps li.wait .dot{background:var(--lv);animation:d2blink 1.2s ease-in-out infinite}
.steps li.ok .dot{background:var(--ok)}
@keyframes d2blink{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.25;transform:scale(1.35)}}
@media (prefers-reduced-motion:reduce){.steps li.wait .n{animation:d2fade 1.6s ease-in-out infinite;transform:none;box-shadow:none}.steps li.wait .dot{animation:d2fade 1.6s ease-in-out infinite}}
@keyframes d2fade{0%,100%{opacity:1}50%{opacity:.35}}
.btn.talk{margin-top:8px}.btn.talk:disabled,.btn.meld:disabled{opacity:1;filter:none;cursor:default;background:color-mix(in srgb,var(--lv) 14%,var(--card));border:1.5px dashed color-mix(in srgb,var(--lv) 80%,var(--line));color:color-mix(in srgb,var(--ink) 80%,var(--card))}.btn.talk:not(:disabled){background:var(--lv);border-color:var(--lv);color:#fff}
.btn.meld{background:var(--lv);border-color:var(--lv);color:#fff}
.btn.meld[aria-pressed="true"]{background:var(--ok);border-color:var(--ok)}
.mm{display:inline-flex;vertical-align:-3px;padding:2px;border-radius:50%;border:1px solid currentColor}
.mwrap{position:relative;display:inline-block;max-width:100%}
.pop{position:absolute;left:0;top:calc(100% + 8px);z-index:5;width:min(320px,calc(100vw - 32px));padding:12px 30px 12px 14px;border-radius:12px;background:var(--card);border:1px solid var(--lv);box-shadow:0 6px 24px rgba(0,0,0,.18);font-size:14px;line-height:1.45}
.pop b:first-of-type{display:block;margin-bottom:4px}.pop::before{content:"";position:absolute;top:-7px;left:22px;width:12px;height:12px;background:var(--card);border-left:1px solid var(--lv);border-top:1px solid var(--lv);transform:rotate(45deg)}
.pop .mm{color:var(--lv)}.px{position:absolute;top:6px;right:8px;border:0;background:none;font-size:18px;line-height:1;color:var(--soft);cursor:pointer}
.also{margin-top:10px;padding:12px 14px;border-radius:12px;border:1px solid color-mix(in srgb,var(--ok) 45%,var(--line));background:color-mix(in srgb,var(--ok) 8%,var(--card))}.also>b{display:block}
.dot{display:inline-block;width:10px;height:10px;border-radius:50%;background:var(--line);margin-right:6px;vertical-align:0}.dot.on{background:var(--ok)}
details{margin-top:6px}summary{cursor:pointer;color:var(--soft);font-size:14px}
</style>
<p class="small"><a href="/page/AgentOnboarding" data-go>← Onboard your AI</a></p>
<h1>Upgrade to a chat loop</h1>
<p>Today you talk to your AI in its own app. In a chat loop it stays on d2 and answers you in the chat panel, beside the page you're looking at. One line connects it and starts the loop.</p>
<ol class="steps">
<li><span class="n">1</span><div><b>Let your AI reach d2</b>Allow <code>*.aiputty.com</code> in its network settings, if it asks.
<details><summary>Claude</summary><p class="small">Settings → Capabilities: add <code>*.aiputty.com</code> to the allowed domains for code execution (or the project's network settings).</p></details>
<p><button class="btn" id="done1" type="button">Done →</button></p></div></li>
<li id="s2" class="col"><span class="n">2</span><div><b>Paste this into a chat with your AI</b><div class="more" hidden>Claude on your desktop, web or phone. That chat becomes your loop.
<pre class="line" id="ln">Making your loop line…</pre>
<button class="btn" id="cp" type="button">Copy</button> <button class="btn" id="nw" type="button">New line</button> <span class="small" id="ex"></span>
<span class="sp-say" id="lsay" role="status"></span></div></div></li>
<li id="s3" class="col"><span class="n">3</span><div><b>Connected</b><div class="more" hidden><span id="st"><span class="dot"></span>Waiting for your AI's loop…</span>
<p class="small">When it's listening, say hello in the chat panel (bottom right, on any page) or open <a href="/pad" data-go>the pad</a>: it answers in a few seconds.</p>
<div class="also" id="also" hidden><b>Another chat, same AI?</b>In any other chat where your AI already works on this project, paste this and it starts a loop there too:
<pre class="line" id="sl">start loop</pre><button class="btn" id="cpsl" type="button">Copy</button>
<p class="small">A chat that has never connected to this project needs its own line: press <b>New line</b> in step 2. To stop a loop, type <i>stop loop</i> in its chat.</p></div></div></div></li>
</ol>
<h2>How the loop works</h2>
<ul>
<li><b>Where you talk:</b> the chat panel on every d2 page, not your AI's app.<br><button class="btn talk" id="talk" type="button" disabled title="Lights up when your AI's loop is listening">💬 TurboChat</button> <span class="small" id="tsay"></span></li>
<li><b>How it asks you things:</b> TurboChat. Its options come as a pad: tap to pick, one <b>CONTINUE</b> sends your answer.</li>
<li><b>While it runs:</b> keep that AI chat open (computer or phone); it uses your AI as usual, only while you chat.</li>
<li><b>Stop any time:</b> type <i>stop loop</i> in that chat, or close it. When its chat gets full, it tells you; make a new line here for a new chat.</li>
</ul>
<h2>Mind Meld: let it show you</h2>
<p>With Mind Meld on, your AI doesn't just tell you what it made: it takes this browser tab to the page it's talking about and outlines what changed, with <b>Back</b> to return to where you were. You keep talking in the chat panel; d2 follows.</p>
<ul>
<li><b>Only your own AIs</b>, and the agents they started, can move your tab. Nobody else's can.</li>
<li><b>Off by default</b>, per tab: the <b>Mind Meld</b> button in the top bar turns it on or off any time, and so does <b>Meld</b> in the chat panel.</li>
</ul>
<div class="mwrap"><button class="btn meld" id="meld" type="button" aria-pressed="false" disabled title="Lights up when your AI's loop is listening"><span class="mm"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="2"/><path d="M7.8 7.8a6 6 0 0 0 0 8.4M16.2 7.8a6 6 0 0 1 0 8.4"/><path d="M4.9 4.9a10 10 0 0 0 0 14.2M19.1 4.9a10 10 0 0 1 0 14.2"/></svg></span> <span id="mlabel">Turn on Mind Meld</span></button> <span class="small" id="msay"></span>
<div class="pop" id="mpop" role="dialog" aria-label="Start a chat first" hidden><button class="px" id="mpx" type="button" aria-label="Close">×</button>
<b>Start your TurboChat first</b>Press <b>💬 TurboChat</b> above, under <i>Where you talk</i>; once its chat is open, click <span class="mm"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="2"/><path d="M7.8 7.8a6 6 0 0 0 0 8.4M16.2 7.8a6 6 0 0 1 0 8.4"/><path d="M4.9 4.9a10 10 0 0 0 0 14.2M19.1 4.9a10 10 0 0 1 0 14.2"/></svg></span> to turn on Mind Meld for it.</div></div>
<h2>If it doesn't connect</h2>
<ul>
<li>No light after a minute: check your AI read the line. A line works once, for an hour: press <b>New line</b> and paste again.</li>
<li>Your AI says it can't reach the site: step 1.</li>
<li>Rather type in your AI's own app? <a href="/page/AgentOnboardPrompt" data-go>Keep using your prompt</a>.</li>
</ul>
<h2>Ready?</h2>
<p>Start playing: your dashboard becomes this project's home.</p>
<p><button class="sp-go" id="play" type="button">ok, start playing →</button><span class="sp-say" id="psay" role="status"></span></p>
</div>
<script>
(()=>{const D2=window.d2||{},$=s=>document.querySelector(s);
const P=Object.assign({},Object.fromEntries(new URLSearchParams(location.search)),D2.params||window.d2params||{});
const api=(m,u,b)=>D2.api?Promise.resolve(D2.api(m,u,b)):Promise.reject(new Error("E_BRIDGE: open this page in d2"));
const go=u=>D2.go?D2.go(u):(location.href=u);
const msg=e=>String(e&&e.message||e).replace(/^E_[A-Z_]+: /,"");
document.addEventListener("click",e=>{const a=e.target.closest&&e.target.closest("a[data-go]");if(!a)return;e.preventDefault();go(a.getAttribute("href"))});
const play=async(b,say)=>{b.disabled=true;say.textContent="";
  try{await api("POST","/api/v2/project/home",{to:"dashboard"})}catch(e){if(!/E_SCOPE|E_FORBIDDEN|403/.test(String(e&&e.message||e))&&!/E_NOT_FOUND/.test(String(e&&e.message||e))){say.textContent=msg(e);b.disabled=false;return}}
  go("/")};
let LOOP="",CHAT=false;const PROJECT=(D2.project||location.hostname.split(".")[0]);let since=new Date().toISOString(),busy=false,until=0;
const make=async()=>{if(busy)return;busy=true;$("#lsay").textContent="";$("#ln").textContent="Making your loop line…";
  try{const r=await api("POST","/api/v2/onboard/setup",{project:PROJECT,kind:"app-loop"});$("#ln").textContent=(r&&r.line)||"No line came back.";
    until=r&&r.expires?Date.parse(r.expires):0;since=new Date().toISOString();$("#s3").classList.remove("ok");$("#s3").classList.add("wait");tick()}
  catch(e){$("#ln").textContent="Couldn't make the line.";$("#lsay").textContent=msg(e)}busy=false};
const tick=()=>{if(!until){$("#ex").textContent="";return}const left=until-Date.now();
  $("#ex").textContent=left>0?"works once, until "+new Date(until).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"}):"expired: press New line"};
$("#cp").onclick=async e=>{try{await navigator.clipboard.writeText($("#ln").textContent);e.target.textContent="Copied"}catch(_){e.target.textContent="Select and copy"}};
$("#nw").onclick=()=>make();
// Mind Meld: the same as the panel's Meld button: keep the open chat, then the top bar's switch (#d2esp); its state is per tab (sessionStorage d2esp)
const meldOn=()=>{try{return sessionStorage.getItem("d2esp")==="1"}catch(e){return false}};
const meldDraw=()=>{const on=meldOn();$("#meld").setAttribute("aria-pressed",String(on));$("#mlabel").textContent=on?"Mind Meld is on ✓ (turn off)":"Turn on Mind Meld"};
// a chat must be open first: this page's chat button, or any open chat in the panel
const chatOpen=()=>{if(CHAT)return true;try{const st=window.d2chat&&window.d2chat.state&&window.d2chat.state();return !!(st&&(st.chats||[]).some(c=>c.state==="live"||c.state==="asked"))}catch(_){return false}};
$("#mpx").onclick=()=>{$("#mpop").hidden=true};
document.addEventListener("click",e=>{if(!$("#mpop").hidden&&!e.target.closest(".mwrap"))$("#mpop").hidden=true});
document.addEventListener("keydown",e=>{if(e.key==="Escape")$("#mpop").hidden=true});
$("#meld").onclick=()=>{const esp=document.getElementById("d2esp");$("#msay").textContent="";
  if(!meldOn()&&!chatOpen()){$("#mpop").hidden=false;return}
  $("#mpop").hidden=true;
  if(!esp){$("#msay").textContent="Turn it on with Mind Meld in the top bar.";return}
  // the open chat's own Meld button when the panel shows one, so the chat's button and the top bar stay in step (it keeps the chat open too)
  const pm=document.querySelector("#d2chat .d2c-meld");if(pm)pm.click();else esp.click();setTimeout(meldDraw,300)};
meldDraw();if(meldOn())$("#meld").disabled=false;
// the floatie on, then a chat with the loop's agent (its role from its name: designer-108 → designer), opened in the panel
$("#talk").onclick=async e=>{const b=e.target;b.disabled=true;$("#tsay").textContent="";const role=(LOOP.match(/^([a-z]+)-\d+$/)||[])[1]||"designer";
  try{await api("PUT","/api/v2/me",{chat:true});
    const subject={kind:"page",ref:location.pathname};
    if(window.d2chat&&window.d2chat.start)await window.d2chat.start({role,agent:LOOP,subject});
    else{await api("POST","/api/v2/chats",{role,agent:LOOP,subject});$("#tsay").textContent="TurboChat started: open it from 💬, bottom right."}
    b.textContent="💬 TurboChat open";CHAT=true;$("#mpop").hidden=true}
  catch(x){$("#tsay").textContent=msg(x);b.disabled=false}};
$("#cpsl").onclick=async e=>{try{await navigator.clipboard.writeText("start loop");e.target.textContent="Copied"}catch(_){e.target.textContent="Select and copy"}};
// the light: GET /api/v2/me/connect → loop {agent, at} once the loop listens
const poll=async()=>{tick();try{const c=await api("GET","/api/v2/me/connect");if(c&&c.loop&&c.loop.agent&&(!c.loop.at||c.loop.at>=since)){$("#st").innerHTML='<span class="dot on"></span>'+String(c.loop.agent).replace(/[<&]/g,"")+" · listening";$("#s3").classList.remove("wait");$("#s3").classList.add("ok");$("#also").hidden=false;LOOP=String(c.loop.agent);$("#talk").disabled=false;$("#meld").disabled=false;$("#meld").title="";return}}catch(_){}
  setTimeout(poll,5000)};
// nothing is made on a visit: step 1's Done opens step 2 and makes the line (razie, 2026-10-12)
$("#done1").onclick=e=>{e.target.disabled=true;e.target.textContent="Done ✓";["#s2","#s3"].forEach(id=>{$(id).classList.remove("col");$(id+" .more").hidden=false});$("#s3").classList.add("wait");make();poll()};
$("#play").onclick=()=>play($("#play"),$("#psay"));
})();
</script>
</body></html>