← back to Resume Queue Viewer
Add Run/Resume action — launches a claude iTerm2 tab per queue item
bf816e72cf503adb96abfec51813c67def862efa · 2026-05-18 17:23:25 -0700 · SteveStudio2
Files touched
Diff
commit bf816e72cf503adb96abfec51813c67def862efa
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Mon May 18 17:23:25 2026 -0700
Add Run/Resume action — launches a claude iTerm2 tab per queue item
---
server.js | 132 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 131 insertions(+), 1 deletion(-)
diff --git a/server.js b/server.js
index 4ba9b92..c3dd24a 100644
--- a/server.js
+++ b/server.js
@@ -5,7 +5,9 @@
const http = require('http');
const fs = require('fs');
+const os = require('os');
const path = require('path');
+const { execFile } = require('child_process');
const PORT = process.env.PORT || 9768;
const CNCP = path.join(process.env.HOME, 'cncp-starter');
@@ -54,6 +56,64 @@ function buildQueue() {
counts: { parking: parking.length, recaps: recaps.length, wins: wins.length } };
}
+// ── Run an item ────────────────────────────────────────────────────────────
+// Opens a fresh iTerm2 tab and boots a `claude` session pre-loaded with a
+// task prompt built from the queue item. Returns a Promise<{ok,message}>.
+function escAS(s) { return String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"'); }
+
+function buildPrompt(item) {
+ if (item.type === 'recap') {
+ return [
+ 'Resume this prior work session from its CNCP recap.',
+ item.path ? 'Recap file: ' + item.path : '',
+ '',
+ item.title,
+ item.body || '',
+ '',
+ 'Read the recap, re-establish full context, then continue where it left off.',
+ ].filter(Boolean).join('\n');
+ }
+ // parking-lot TODO (default)
+ return [
+ 'Resume-queue item from the CNCP parking lot — work this now.',
+ '',
+ 'TITLE: ' + item.title,
+ 'NOTE: ' + (item.body || ''),
+ item.link ? 'REF: ' + item.link : '',
+ '',
+ 'Investigate, propose a concrete approach, confirm anything risky with Steve, then proceed.',
+ ].filter(Boolean).join('\n');
+}
+
+function runItem(item) {
+ return new Promise((resolve) => {
+ const prompt = buildPrompt(item);
+ // Stash the prompt in a temp file so no shell/AppleScript escaping of the
+ // prompt body is needed — the tab just cats it into `claude`.
+ const tmp = path.join(os.tmpdir(), 'rq-run-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8) + '.txt');
+ try { fs.writeFileSync(tmp, prompt, 'utf8'); }
+ catch (e) { return resolve({ ok: false, message: 'temp write failed: ' + e.message }); }
+
+ const home = process.env.HOME;
+ const shellCmd = "cd '" + home + "' && claude \"$(cat '" + tmp + "')\"; rm -f '" + tmp + "'";
+ const script =
+ 'tell application "iTerm2"\n' +
+ ' activate\n' +
+ ' if (count of windows) = 0 then\n' +
+ ' create window with default profile\n' +
+ ' else\n' +
+ ' tell current window to create tab with default profile\n' +
+ ' end if\n' +
+ ' tell current session of current window to write text "' + escAS(shellCmd) + '"\n' +
+ 'end tell';
+
+ execFile('osascript', ['-e', script], { timeout: 15000 }, (err, stdout, stderr) => {
+ if (err) return resolve({ ok: false, message: (stderr || err.message || 'osascript failed').trim() });
+ resolve({ ok: true, message: 'Launched a claude tab for "' + item.title + '"' });
+ });
+ });
+}
+
const PAGE = `<!doctype html>
<html lang="en">
<head>
@@ -98,6 +158,17 @@ const PAGE = `<!doctype html>
.card a { color:var(--accent); text-decoration:none; }
.stars { color:#f5c451; }
.empty { color:var(--dim); padding:40px; text-align:center; }
+ .card .actions { display:flex; gap:8px; }
+ button.run { background:var(--accent); color:#fff; border:none; border-radius:7px;
+ padding:7px 14px; font-size:12.5px; font-weight:600; cursor:pointer; }
+ button.run:hover { filter:brightness(1.12); }
+ button.run:disabled { opacity:.55; cursor:progress; }
+ #toast { position:fixed; left:50%; bottom:26px; transform:translateX(-50%) translateY(20px);
+ background:#1f2230; border:1px solid var(--line); color:var(--txt);
+ padding:11px 18px; border-radius:9px; font-size:13px; opacity:0;
+ transition:.25s; pointer-events:none; max-width:80vw; }
+ #toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
+ #toast.err { border-color:#7a2336; }
</style>
</head>
<body>
@@ -121,6 +192,7 @@ const PAGE = `<!doctype html>
<input type="range" id="dens" min="1" max="5" value="3">
</div>
<main><div class="grid" id="grid"></div></main>
+<div id="toast"></div>
<script>
let DATA = { parking:[], recaps:[], wins:[] };
let filter = localStorage.getItem('rq.filter') || 'all';
@@ -140,21 +212,58 @@ function render() {
const g = document.getElementById('grid');
if (!items.length) { g.innerHTML = '<div class="empty">Nothing in the queue.</div>'; return; }
- g.innerHTML = items.map(i => {
+ window.LASTITEMS = items;
+ g.innerHTML = items.map((i, idx) => {
const stars = i.type==='win' ? '<span class="stars">'+'★'.repeat(i.rank||3)+'</span>' : '';
const link = i.link ? '<a href="'+esc(i.link)+'" target="_blank">open ↗</a>' : '';
const proj = i.project ? esc(i.project) : '';
+ const runnable = i.type==='parking' || i.type==='recap';
+ const runLabel = i.type==='recap' ? 'Resume ▸' : 'Run ▸';
+ const actions = runnable
+ ? '<div class="actions"><button class="run" data-idx="'+idx+'">'+runLabel+'</button></div>'
+ : '';
return '<div class="card">'
+ '<div class="top"><span class="tag '+TAGCLASS[i.type]+'">'+i.type+'</span>'
+ '<span class="badge '+(i.badge==='open'?'open':i.badge==='done'?'done':'')+'">'+esc(i.badge)+'</span></div>'
+ '<h3>'+esc(i.title)+'</h3>'
+ '<p>'+esc(i.body)+'</p>'
+ + actions
+ '<div class="meta">'+(i.date?'<span>'+esc(i.date)+'</span>':'')
+ (proj?'<span>'+proj+'</span>':'') + stars + link + '</div>'
+ '</div>';
}).join('');
}
+let toastTimer;
+function toast(msg, isErr) {
+ const t = document.getElementById('toast');
+ t.textContent = msg;
+ t.className = 'show' + (isErr ? ' err' : '');
+ clearTimeout(toastTimer);
+ toastTimer = setTimeout(() => { t.className = ''; }, 4200);
+}
+
+async function runItem(idx, btn) {
+ const item = (window.LASTITEMS || [])[idx];
+ if (!item) return;
+ btn.disabled = true;
+ const orig = btn.textContent;
+ btn.textContent = 'Launching…';
+ try {
+ const r = await fetch('/api/run', {
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(item),
+ });
+ const j = await r.json();
+ toast(j.message || (j.ok ? 'launched' : 'failed'), !j.ok);
+ } catch (e) {
+ toast('request failed: ' + e.message, true);
+ } finally {
+ btn.disabled = false;
+ btn.textContent = orig;
+ }
+}
+
function setFilter(f){ filter=f; localStorage.setItem('rq.filter',f);
document.querySelectorAll('.bar button').forEach(b=>b.classList.toggle('on',b.dataset.f===f));
render(); }
@@ -173,6 +282,11 @@ document.querySelectorAll('.bar button').forEach(b=>b.onclick=()=>setFilter(b.da
document.getElementById('sort').onchange = e=>{ sort=e.target.value; localStorage.setItem('rq.sort',sort); render(); };
document.getElementById('dens').oninput = e=>{ dens=+e.target.value; localStorage.setItem('rq.dens',dens); render(); };
+document.getElementById('grid').addEventListener('click', e => {
+ const btn = e.target.closest('button.run');
+ if (btn) runItem(+btn.dataset.idx, btn);
+});
+
document.getElementById('sort').value = sort;
document.getElementById('dens').value = dens;
setFilter(filter);
@@ -188,6 +302,22 @@ const server = http.createServer((req, res) => {
res.end(JSON.stringify(buildQueue()));
return;
}
+ if (req.url === '/api/run' && req.method === 'POST') {
+ let body = '';
+ req.on('data', c => { body += c; if (body.length > 1e5) req.destroy(); });
+ req.on('end', async () => {
+ let item;
+ try { item = JSON.parse(body || '{}'); }
+ catch (e) { res.writeHead(400, { 'Content-Type': 'application/json' });
+ return res.end(JSON.stringify({ ok: false, message: 'bad JSON' })); }
+ if (!item || !item.title) { res.writeHead(400, { 'Content-Type': 'application/json' });
+ return res.end(JSON.stringify({ ok: false, message: 'missing item' })); }
+ const result = await runItem(item);
+ res.writeHead(result.ok ? 200 : 500, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify(result));
+ });
+ return;
+ }
if (req.url === '/healthz') { res.writeHead(200); res.end('ok'); return; }
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(PAGE);
← 598f4ad initial scaffold — resume queue viewer (CNCP parking-lot + r
·
back to Resume Queue Viewer
·
Add launchd plist reference copy for auto-start eaf6280 →