---
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}}
.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.</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>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("/")};
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();
$("#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;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>