[object Object]

← back to Model Arena

arena: Build-in-iTerm button per challenge (+ detail view) — writes the challenge brief to a temp file and opens a real interactive Claude session in a new iTerm2 window to build it; safe file-based prompt escaping

584400e60b842c3bf46699e17d8f5d4a60442d3d · 2026-07-23 06:45:08 -0700 · Steve

Files touched

Diff

commit 584400e60b842c3bf46699e17d8f5d4a60442d3d
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 23 06:45:08 2026 -0700

    arena: Build-in-iTerm button per challenge (+ detail view) — writes the challenge brief to a temp file and opens a real interactive Claude session in a new iTerm2 window to build it; safe file-based prompt escaping
---
 data/artifacts/be56e35a6067/claude-code.html | 138 +++++++++++++++++++++++++++
 data/artifacts/be56e35a6067/claude-code.png  | Bin 0 -> 197060 bytes
 data/challenges.json                         |  31 ++++++
 public/index.html                            |  21 +++-
 server.js                                    |  24 +++++
 5 files changed, 212 insertions(+), 2 deletions(-)

diff --git a/data/artifacts/be56e35a6067/claude-code.html b/data/artifacts/be56e35a6067/claude-code.html
new file mode 100644
index 0000000..ce241a3
--- /dev/null
+++ b/data/artifacts/be56e35a6067/claude-code.html
@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Daily Inspiration</title>
+<style>
+  * { margin: 0; padding: 0; box-sizing: border-box; }
+
+  body {
+    min-height: 100vh;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
+    font-family: Georgia, 'Times New Roman', serif;
+    color: #eaeaea;
+    padding: 2rem;
+  }
+
+  .card {
+    max-width: 640px;
+    width: 100%;
+    text-align: center;
+  }
+
+  h1 {
+    font-size: 0.85rem;
+    letter-spacing: 0.35em;
+    text-transform: uppercase;
+    color: #8fa3c8;
+    font-family: 'Helvetica Neue', Arial, sans-serif;
+    font-weight: 400;
+    margin-bottom: 2.5rem;
+  }
+
+  #quote-box {
+    min-height: 10rem;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    opacity: 1;
+    transition: opacity 0.35s ease;
+  }
+
+  #quote-box.fading { opacity: 0; }
+
+  blockquote {
+    font-size: 1.7rem;
+    line-height: 1.5;
+    font-style: italic;
+    quotes: "\201C" "\201D";
+  }
+
+  blockquote::before { content: open-quote; color: #e0a458; }
+  blockquote::after  { content: close-quote; color: #e0a458; }
+
+  cite {
+    display: block;
+    margin-top: 1.25rem;
+    font-style: normal;
+    font-size: 1rem;
+    letter-spacing: 0.08em;
+    color: #e0a458;
+    font-family: 'Helvetica Neue', Arial, sans-serif;
+  }
+
+  cite::before { content: "— "; }
+
+  .placeholder {
+    font-size: 1.25rem;
+    color: #7d8bb0;
+    font-style: italic;
+  }
+
+  button {
+    margin-top: 3rem;
+    padding: 0.9rem 2.4rem;
+    font-size: 1rem;
+    letter-spacing: 0.15em;
+    text-transform: uppercase;
+    font-family: 'Helvetica Neue', Arial, sans-serif;
+    color: #1a1a2e;
+    background: #e0a458;
+    border: none;
+    border-radius: 999px;
+    cursor: pointer;
+    transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
+    box-shadow: 0 4px 18px rgba(224, 164, 88, 0.35);
+  }
+
+  button:hover {
+    background: #eab876;
+    transform: translateY(-2px);
+    box-shadow: 0 8px 24px rgba(224, 164, 88, 0.45);
+  }
+
+  button:active { transform: translateY(0); }
+</style>
+</head>
+<body>
+  <main class="card">
+    <h1>Daily Inspiration</h1>
+    <div id="quote-box">
+      <p class="placeholder">Press the button to receive a spark of inspiration.</p>
+    </div>
+    <button id="quote-btn" type="button">Inspire Me</button>
+  </main>
+
+<script>
+  var quotes = [
+    { text: "The only way to do great work is to love what you do.", author: "Steve Jobs" },
+    { text: "It always seems impossible until it is done.", author: "Nelson Mandela" },
+    { text: "In the middle of difficulty lies opportunity.", author: "Albert Einstein" },
+    { text: "What you get by achieving your goals is not as important as what you become by achieving your goals.", author: "Zig Ziglar" },
+    { text: "The best time to plant a tree was twenty years ago. The second best time is now.", author: "Chinese Proverb" }
+  ];
+
+  var box = document.getElementById('quote-box');
+  var btn = document.getElementById('quote-btn');
+  var lastIndex = -1;
+
+  btn.addEventListener('click', function () {
+    var i;
+    do {
+      i = Math.floor(Math.random() * quotes.length);
+    } while (i === lastIndex && quotes.length > 1);
+    lastIndex = i;
+
+    box.classList.add('fading');
+    setTimeout(function () {
+      box.innerHTML = '<blockquote>' + quotes[i].text + '</blockquote><cite>' + quotes[i].author + '</cite>';
+      box.classList.remove('fading');
+    }, 350);
+  });
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/be56e35a6067/claude-code.png b/data/artifacts/be56e35a6067/claude-code.png
new file mode 100644
index 0000000..e4068fb
Binary files /dev/null and b/data/artifacts/be56e35a6067/claude-code.png differ
diff --git a/data/challenges.json b/data/challenges.json
index 5bd0ee5..58463b2 100644
--- a/data/challenges.json
+++ b/data/challenges.json
@@ -1443,5 +1443,36 @@
         "thumb": true
       }
     ]
+  },
+  {
+    "id": "be56e35a6067",
+    "title": "Max Plan Claude Test",
+    "prompt": "Single HTML file: a button that, when clicked, shows a random inspirational quote from an array of 5. Center it, nice typography.",
+    "category": "Custom",
+    "created_at": "2026-07-23T13:41:16.707Z",
+    "winner": null,
+    "runs": [
+      {
+        "model": "claude-code",
+        "status": "done",
+        "error": null,
+        "seconds": 21,
+        "cost": 0,
+        "started_at": "2026-07-23T13:41:16.708Z",
+        "finished_at": "2026-07-23T13:41:37.779Z",
+        "bytes": 3624,
+        "thumb": true,
+        "aiScore": 8.3,
+        "aiReason": "The design is clean and the button effectively fulfills the challenge of showing a random quote.",
+        "aiScores": {
+          "qwen2.5vl:7b": 9,
+          "minicpm-v:latest": 7.5
+        },
+        "aiSpread": 1.5
+      }
+    ],
+    "judging": false,
+    "aiPick": "claude-code",
+    "judged_at": "2026-07-23T13:41:50.912Z"
   }
 ]
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index 47559b6..30d7a17 100644
--- a/public/index.html
+++ b/public/index.html
@@ -47,6 +47,11 @@ input[type=range]{accent-color:var(--neon);width:130px}
 .chip.queued{border-color:var(--dim)}
 .chip.error{border-color:var(--err);color:var(--err)}
 .chip.winner{border-color:var(--gold);color:var(--gold);font-weight:700}
+.mini.build{margin-top:10px;background:none;border:1px solid var(--neon2);color:var(--neon2);padding:5px 12px;font:inherit;font-size:11px;
+  letter-spacing:1px;text-transform:uppercase;cursor:pointer;border-radius:3px;width:100%}
+.mini.build:hover{background:rgba(255,46,166,.12);box-shadow:0 0 10px rgba(255,46,166,.25)}
+.mini.build:disabled{opacity:.6;cursor:default}
+#d-build{color:var(--neon2)}
 @keyframes pulse{50%{opacity:.35}}
 .empty{color:var(--dim);padding:40px;text-align:center;border:1px dashed var(--line)}
 /* new challenge */
@@ -181,6 +186,7 @@ tr.clk{cursor:pointer}tr.clk:hover td{background:rgba(0,229,255,.06)}
   <section id="detail" class="view">
     <span class="back" id="back">← back to challenges</span>
     <a class="back" id="export" style="margin-left:16px" download>⬇ export battle as standalone HTML</a>
+    <span class="back" id="d-build" style="margin-left:16px">⚒ Build in iTerm</span>
     <label class="blindtog"><input type="checkbox" id="blind"> 🕶 Blind judging — hide model names until a winner is crowned (kills brand bias)</label>
     <h2 id="d-title"></h2>
     <div class="meta" id="d-meta"></div>
@@ -285,12 +291,22 @@ async function loadList(){
     div.innerHTML = `<h3>${esc(c.title)}</h3>
       <div class="when" title="${c.created_at}">🕓 ${fmtWhen(c.created_at)}</div>
       ${shotsHtml}
-      <div class="chips">${chips}</div>`;
-    div.onclick = ()=>openDetail(c.id);
+      <div class="chips">${chips}</div>
+      <button class="mini build" data-build="${c.id}" title="Open a real Claude session in iTerm2, pre-loaded with this challenge, to actually build it">⚒ Build in iTerm</button>`;
+    div.onclick = (e)=>{ if(e.target.dataset.build) return; openDetail(c.id); };
+    div.querySelector('[data-build]').onclick = (e)=>{ e.stopPropagation(); buildInIterm(c.id, e.target); };
     el.appendChild(div);
   }
 }
 function esc(s){ return String(s).replace(/[&<>"]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
+async function buildInIterm(id, btn){
+  const t = btn && btn.textContent; if(btn){ btn.textContent='⚒ launching…'; btn.disabled=true; }
+  try{
+    const r = await fetch(API+'/api/challenges/'+id+'/build',{method:'POST'});
+    if(!r.ok){ alert('Build launch failed: '+(await r.text())); }
+    else if(btn){ btn.textContent='⚒ launched ↗'; setTimeout(()=>{btn.textContent=t;btn.disabled=false;},2500); }
+  }catch(e){ alert('Build launch failed: '+e.message); if(btn){btn.textContent=t;btn.disabled=false;} }
+}
 
 // ---- new battle ----
 async function loadModels(){
@@ -356,6 +372,7 @@ async function renderDetail(id, statusOnly){
   const remixNote = c.remixOf ? '🧬 remixed from ' + mLabel(c.remixSource) + "'s artifact\n" : '';
   $('#d-meta').textContent = '🕓 ' + fmtWhen(c.created_at) + (c.category?'  ·  '+c.category:'') + '\n' + remixNote + c.prompt;
   $('#export').href = API+'/export/'+c.id;
+  $('#d-build').onclick = (e)=>buildInIterm(c.id, e.target);
   // AI referee status bar
   const anyArt = c.runs.some(r=>r.status==='done'&&r.thumb);
   const ai = $('#d-ai');
diff --git a/server.js b/server.js
index fbb6793..3ee7baa 100644
--- a/server.js
+++ b/server.js
@@ -507,6 +507,30 @@ const server = http.createServer(async (req, res) => {
     return send(res, 201, nc);
   }
 
+  if ((m = p.match(/^\/api\/challenges\/([a-f0-9]+)\/build$/)) && req.method === 'POST') {
+    const c = challenges.find(x => x.id === m[1]);
+    if (!c) return send(res, 404, { error: 'not found' });
+    if (process.platform !== 'darwin') return send(res, 400, { error: 'iTerm launch is macOS-only' });
+    // write the brief to a temp file so any quotes/newlines in the prompt can't break escaping
+    const tmp = '/tmp/arena-build-' + c.id + '.md';
+    const brief = `Build this Model Arena challenge as a real, working project.\n\nTITLE: ${c.title}\nCATEGORY: ${c.category || 'Custom'}\n\nCHALLENGE:\n${c.prompt}\n\nDeliver a complete, working result (a single-file HTML unless it clearly needs more). When done, open it in a browser to verify it works.`;
+    try { fs.writeFileSync(tmp, brief); } catch (e) { return send(res, 500, { error: 'temp write failed' }); }
+    const safeTitle = c.title.replace(/[^\w \-]/g, '').slice(0, 40);
+    // iTerm runs: cd ~/Projects, print a banner, launch interactive claude seeded with the brief
+    const payload = `cd "${os.homedir()}/Projects" && clear && printf '\\n==== ARENA BUILD: ${safeTitle} ====\\n\\n' && claude "$(cat ${tmp})"`;
+    const osaEsc = s => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
+    const script = `tell application "iTerm"
+  activate
+  set w to (create window with default profile)
+  tell current session of w
+    set name to "arena: ${osaEsc(safeTitle)}"
+    write text "${osaEsc(payload)}"
+  end tell
+end tell`;
+    execFile('osascript', ['-e', script], { timeout: 15000 }, (err) => { if (err) console.error('[build] osascript:', err.message); });
+    return send(res, 200, { ok: true, launched: c.id });
+  }
+
   if ((m = p.match(/^\/api\/challenges\/([a-f0-9]+)\/vote$/)) && req.method === 'POST') {
     const c = challenges.find(x => x.id === m[1]);
     if (!c) return send(res, 404, { error: 'not found' });

← e93e641 arena: Claude via Max plan (Claude Code CLI adapter, $0 subs  ·  back to Model Arena  ·  auto-save: 2026-07-23T06:49:07 (7 files) — data/challenges.j 2897af6 →