← back to Ticket Action Viewer
viewer: add RUN NOW — opens a new iTerm2 window with a Claude session that takes + drives the ticket (gates intact)
dccd5abccfd3f5816f9dbb240cd7e46cc7751dbe · 2026-08-10 07:56:29 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M public/index.htmlM server.js
Diff
commit dccd5abccfd3f5816f9dbb240cd7e46cc7751dbe
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 10 07:56:29 2026 -0700
viewer: add RUN NOW — opens a new iTerm2 window with a Claude session that takes + drives the ticket (gates intact)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
public/index.html | 8 ++++++++
server.js | 18 ++++++++++++++++++
2 files changed, 26 insertions(+)
diff --git a/public/index.html b/public/index.html
index 7085279..b3186d9 100644
--- a/public/index.html
+++ b/public/index.html
@@ -31,6 +31,8 @@
button.k{background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:7px;padding:5px 9px;font:600 12px/1 inherit;cursor:pointer}
button.k:hover{border-color:var(--acc)}
button.k.done{color:var(--ok)}button.k.take{color:var(--acc)}button.k.block{color:var(--gate)}
+ button.k.run{background:var(--acc);border-color:var(--acc);color:#fff;font-weight:700}
+ button.k.run:hover{background:#3a7bff}
#modal{position:fixed;inset:0;background:#000a;display:none;align-items:center;justify-content:center;z-index:20;padding:24px}
#modal .box{background:var(--card);border:1px solid var(--line);border-radius:12px;max-width:820px;width:100%;max-height:82vh;overflow:auto;padding:18px}
#modal pre{white-space:pre-wrap;font:12px/1.55 ui-monospace,Menlo,monospace;color:#cfd6e2}
@@ -79,6 +81,7 @@ function render(){
<td class="next">${esc(t.nextStep)}${t.note?`<div class="proj" style="margin-top:4px">↳ ${esc(t.note)}</div>`:''}</td>
<td><div class="needs ${t.gated?'gated':''}">${t.gated?'🔒 ':''}${esc(t.needs||'—')}</div></td>
<td><div class="acts">
+ <button class="k run" onclick="runNow('${t.fullId}','${esc(t.title).replace(/'/g,'’')}','${esc(t.project)}')">▶ RUN NOW</button>
<button class="k" onclick="detail('${t.fullId}','${t.shortId}')">Detail</button>
<button class="k take" onclick="act('${t.fullId}','take')">Take</button>
<button class="k" onclick="logIt('${t.fullId}')">+Log</button>
@@ -90,6 +93,11 @@ function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','
async function detail(id,sid){el('mtitle').textContent=sid;el('mbody').textContent='loading…';el('modal').style.display='flex';
const r=await fetch('/api/ticket?id='+encodeURIComponent(id));const d=await r.json();el('mbody').textContent=d.out||'(no output)';}
function closeM(){el('modal').style.display='none';}
+async function runNow(id,title,project){
+ if(!confirm('RUN '+id.match(/^TK-\d+/)[0]+' in a NEW iTerm2 window? A fresh Claude session will take it and start work.'))return;
+ toast('opening iTerm2…');
+ const r=await fetch('/api/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,title,project})});
+ const d=await r.json();toast(d.ok?('▶ '+d.out):('✗ '+d.out.slice(0,90)));}
async function act(id,action){if((action==='done'||action==='block')&&!confirm(`${action} ${id.match(/^TK-\\d+/)[0]}?`))return;
const r=await fetch('/api/action',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,action})});
const d=await r.json();toast(d.ok?`✓ ${action} ok`:`✗ ${d.out.slice(0,80)}`);load();}
diff --git a/server.js b/server.js
index 935cd5b..b46e8fd 100644
--- a/server.js
+++ b/server.js
@@ -70,6 +70,24 @@ const server = http.createServer(async (req, res) => {
}
if (u.pathname === '/api/tickets') { return json(res, 200, { agent: AGENT, tickets: await getTickets() }); }
if (u.pathname === '/api/ticket') { const r = await tk(['show', u.searchParams.get('id') || '']); return json(res, 200, r); }
+ if (u.pathname === '/api/run' && req.method === 'POST') {
+ let p; try { p = JSON.parse(await body(req)); } catch { return json(res, 400, { ok: false, out: 'bad json' }); }
+ const shortId = (String(p.id || '').match(/^TK-[0-9]+/) || [''])[0];
+ if (!shortId) return json(res, 400, { ok: false, out: 'bad id' });
+ const title = String(p.title || '').replace(/["`$\\]/g, ' ').slice(0, 160);
+ const project = String(p.project || '').replace(/[^A-Za-z0-9_-]/g, '');
+ // resolve project dir: ~/Projects/<project> if it exists, else ~/Projects
+ const home = process.env.HOME;
+ let dir = path.join(home, 'Projects');
+ if (project && fs.existsSync(path.join(home, 'Projects', project))) dir = path.join(home, 'Projects', project);
+ const sh = path.join('/tmp', `tav-run-${shortId}.sh`);
+ const prompt = `Work ticket ${p.id} (${title}). First run: tk take ${p.id}; tk log ${p.id} "▶ STARTED via RUN NOW". Then read tk show ${p.id}, decide the single highest-value next step, and drive it. Keep gates intact — any customer-facing / deploy / Apple / USPTO / money / prod action drafts to ~/.claude/yolo-queue/pending-approval, never auto-execute.`;
+ fs.writeFileSync(sh, `#!/bin/bash\ncd ${dir}\nexport TK_AGENT=${AGENT}\nexec claude ${JSON.stringify(prompt)}\n`);
+ fs.chmodSync(sh, 0o755);
+ const osa = `tell application "iTerm2"\n activate\n create window with default profile\n tell current session of current window\n write text "clear; echo '▶ RUN ${shortId} — ${title.replace(/'/g, '')}'; echo 'dir: ${dir}'; bash ${sh}"\n end tell\nend tell`;
+ return execFile('osascript', ['-e', osa], (err, so, se) =>
+ json(res, 200, { ok: !err, out: err ? String(se || err) : `Opened iTerm2 window running ${shortId} in ${dir}`, dir }));
+ }
if (u.pathname === '/api/action' && req.method === 'POST') {
let p; try { p = JSON.parse(await body(req)); } catch { return json(res, 400, { ok: false, out: 'bad json' }); }
const { id, action, text } = p;
← 292eb42 viewer: Take now stamps start date+time into the ticket log
·
back to Ticket Action Viewer
·
viewer: RUN NOW reserves+timestamps the ticket at click, the c465d6a →