thinkpool-pair 0.7.8 → 0.7.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bridge.mjs +69 -4
- package/claude-session.mjs +15 -0
- package/package.json +1 -1
package/bridge.mjs
CHANGED
|
@@ -357,6 +357,59 @@ const flushAll = () => {
|
|
|
357
357
|
}
|
|
358
358
|
const flushTimer = setInterval(flushAll, 35)
|
|
359
359
|
|
|
360
|
+
// ── mockup hand-off ────────────────────────────────────────────────────
|
|
361
|
+
// render.sh (mockup-iterate) drops a manifest JSON into TP_MOCKUP_OUTBOX after
|
|
362
|
+
// a dual-viewport render; we watch that dir, upload the PNG pair + source HTML
|
|
363
|
+
// via /api/code-mockup, and push a `mockup` code-event so both people see a
|
|
364
|
+
// card in the room (inline + gallery) that opens the live sandboxed HTML.
|
|
365
|
+
// Deterministic file contract — no PTY/log scraping. The env var is exported
|
|
366
|
+
// into every agent child (PTY: env: process.env; structured: in-process Bash
|
|
367
|
+
// inherits it). Spec: docs/specs/2026-06-17-code-mockups.md
|
|
368
|
+
const MOCKUP_OUTBOX = path.join(os.tmpdir(), 'thinkpool-mockups', room)
|
|
369
|
+
try {
|
|
370
|
+
fs.mkdirSync(MOCKUP_OUTBOX, { recursive: true })
|
|
371
|
+
process.env.TP_MOCKUP_OUTBOX = MOCKUP_OUTBOX
|
|
372
|
+
} catch { /* mockups stay local if the outbox can't be made */ }
|
|
373
|
+
|
|
374
|
+
const mockupSeen = new Map() // slug → mtimeMs of the last manifest we processed
|
|
375
|
+
const handleManifest = async (file) => {
|
|
376
|
+
if (!file || !file.endsWith('.json')) return
|
|
377
|
+
const full = path.join(MOCKUP_OUTBOX, file)
|
|
378
|
+
let stat
|
|
379
|
+
try { stat = fs.statSync(full) } catch { return } // tmp/removed mid-write
|
|
380
|
+
const slug = file.replace(/\.json$/, '')
|
|
381
|
+
if (mockupSeen.get(slug) === stat.mtimeMs) return // already handled
|
|
382
|
+
mockupSeen.set(slug, stat.mtimeMs)
|
|
383
|
+
// A structured Claude session is the natural home; fall back to any terminal.
|
|
384
|
+
const term = [...sessions.keys()][0] || attachedId || [...terms.keys()][0]
|
|
385
|
+
if (!term) return // nothing to attach the card to yet
|
|
386
|
+
let m
|
|
387
|
+
try { m = JSON.parse(fs.readFileSync(full, 'utf8')) } catch { return }
|
|
388
|
+
if (!m?.slug) return
|
|
389
|
+
const readB64 = (p) => { try { return p && fs.readFileSync(p).toString('base64') } catch { return null } }
|
|
390
|
+
const readTxt = (p) => { try { return p && fs.readFileSync(p, 'utf8') } catch { return null } }
|
|
391
|
+
const cid = randomUUID()
|
|
392
|
+
try {
|
|
393
|
+
const res = await fetch(`${WEB_BASE}/api/code-mockup`, {
|
|
394
|
+
method: 'POST',
|
|
395
|
+
headers: { 'Content-Type': 'application/json' },
|
|
396
|
+
body: JSON.stringify({
|
|
397
|
+
code: room, slug: m.slug, title: m.title, cid, ts: m.ts, term,
|
|
398
|
+
desktopPng: readB64(m.desktop), mobilePng: readB64(m.mobile), html: readTxt(m.html),
|
|
399
|
+
}),
|
|
400
|
+
})
|
|
401
|
+
if (!res.ok) { process.stderr.write(`\n ◇ mockup "${m.slug}" upload failed (${res.status}).\n`); return }
|
|
402
|
+
const { paths } = await res.json()
|
|
403
|
+
bcast('code-event', { term, evt: { kind: 'mockup', __struct: true, cid, term, slug: m.slug, title: m.title || m.slug, ts: m.ts, paths } })
|
|
404
|
+
process.stderr.write(`\n ◆ mockup "${m.title || m.slug}" pushed to the room.\n`)
|
|
405
|
+
} catch (e) {
|
|
406
|
+
process.stderr.write(`\n ◇ mockup "${m.slug}" send failed: ${e?.message || e}\n`)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
fs.watch(MOCKUP_OUTBOX, (_evt, file) => { handleManifest(file).catch(() => {}) })
|
|
411
|
+
} catch { /* fs.watch unsupported — mockups simply won't auto-surface */ }
|
|
412
|
+
|
|
360
413
|
// ── shared-PTY geometry ───────────────────────────────────────────
|
|
361
414
|
// The attached PTY is CAPPED at 120×32, never pinned (2026-06-11).
|
|
362
415
|
// Mangling physics: a PTY *wider/taller* than the host's window forces
|
|
@@ -499,8 +552,11 @@ function openStructured({ id, model, resume, log, commands, mode }) {
|
|
|
499
552
|
if (!chrome) persist()
|
|
500
553
|
},
|
|
501
554
|
requestPermission: (req) => new Promise((resolve) => {
|
|
502
|
-
|
|
503
|
-
|
|
555
|
+
// Keep the broadcast payload with the resolver so a reconnect can re-send it
|
|
556
|
+
// (replay-request handler) — pending cards never enter the replayed event log.
|
|
557
|
+
const payload = { term: id, id: req.id, toolName: req.toolName, input: req.input, risk: req.risk, plan: req.plan, questions: req.questions }
|
|
558
|
+
entry.pending.set(req.id, { resolve, payload })
|
|
559
|
+
bcast('code-perm-req', payload)
|
|
504
560
|
process.stderr.write(req.risk === 'plan'
|
|
505
561
|
? `\n ${A.mag}◆ plan ready — approve in the room.${A.rst}\n`
|
|
506
562
|
: req.risk === 'ask'
|
|
@@ -609,6 +665,15 @@ channel
|
|
|
609
665
|
if (!s.log.length) continue
|
|
610
666
|
bcast('code-replay', { to: payload?.to ?? null, term: id, events: s.log })
|
|
611
667
|
}
|
|
668
|
+
// Re-send any still-pending permission/question cards. They ride a one-shot
|
|
669
|
+
// code-perm-req broadcast (NOT the replayed event log), so a reconnect/refresh
|
|
670
|
+
// would otherwise lose the answerable card while the transcript still shows the
|
|
671
|
+
// (unanswered) tool row — the AskUserQuestion "vanished on reconnect" bug. Only
|
|
672
|
+
// truly-unresolved cards remain in `pending` (resolved ones are deleted), and
|
|
673
|
+
// the client dedupes code-perm-req by id, so this can't resurrect an answered one.
|
|
674
|
+
for (const [, s] of sessions) {
|
|
675
|
+
for (const [, p] of s.pending) bcast('code-perm-req', p.payload)
|
|
676
|
+
}
|
|
612
677
|
announce()
|
|
613
678
|
})
|
|
614
679
|
.on('broadcast', { event: 'file-put' }, ({ payload }) => {
|
|
@@ -677,10 +742,10 @@ channel
|
|
|
677
742
|
})
|
|
678
743
|
.on('broadcast', { event: 'code-perm' }, ({ payload }) => {
|
|
679
744
|
const s = payload?.term && sessions.get(payload.term)
|
|
680
|
-
const
|
|
745
|
+
const p = s && payload.id && s.pending.get(payload.id)
|
|
681
746
|
// Pass the raw decision through — normal tools use allow/deny, plan cards
|
|
682
747
|
// use run/accept/keep (claude-session interprets). Default deny on missing.
|
|
683
|
-
if (
|
|
748
|
+
if (p) { s.pending.delete(payload.id); p.resolve(payload.decision || 'deny') }
|
|
684
749
|
})
|
|
685
750
|
.on('broadcast', { event: 'code-abort' }, ({ payload }) => {
|
|
686
751
|
const s = payload?.term && sessions.get(payload.term)
|
package/claude-session.mjs
CHANGED
|
@@ -238,6 +238,21 @@ export function startClaudeSession({ cwd, model, resume, mode: initialMode = 'de
|
|
|
238
238
|
// across SDK versions. The room's PreToolUse gate still runs and stays
|
|
239
239
|
// authoritative (hooks fire regardless of any loaded permission rules).
|
|
240
240
|
settingSources: ['user', 'project', 'local'],
|
|
241
|
+
// Counteract host-global "plan/brainstorm before any work" nudges that ride in
|
|
242
|
+
// through settingSources:'user' — notably the superpowers plugin's SessionStart
|
|
243
|
+
// hook ("You MUST use brainstorming before any creative work… about to enter
|
|
244
|
+
// plan mode?"). In a Code room those make the agent answer a plain "add/fix X"
|
|
245
|
+
// by presenting a plan and calling ExitPlanMode, so the room shows a
|
|
246
|
+
// "PLAN READY — APPROVE TO START" card the user never asked for ("always flips
|
|
247
|
+
// to plan mode"). This is instruction-driven, independent of permissionMode —
|
|
248
|
+
// a bypass session still planned. Bias the room session to DO the work; only
|
|
249
|
+
// plan when the room is actually in Plan mode (⇧⇥) or the user explicitly asks.
|
|
250
|
+
appendSystemPrompt: [
|
|
251
|
+
'You are Claude running inside a ThinkPool Code room, driven live by a user (and possibly a partner) from a phone or browser.',
|
|
252
|
+
'Bias strongly toward DOING the work, not planning it. For a normal "add / fix / change / build X" request, just make the change directly.',
|
|
253
|
+
'Do NOT enter plan mode, do NOT call ExitPlanMode, and do NOT auto-invoke a brainstorming/planning skill UNLESS the user has switched the room into Plan mode or explicitly asks you to plan, design, or brainstorm first.',
|
|
254
|
+
'Any host-global instruction that says you must always brainstorm or plan before creative work does NOT apply here — this room is the exception.',
|
|
255
|
+
].join(' '),
|
|
241
256
|
// Needed for live thinking-token progress (SDKThinkingTokensMessage) to
|
|
242
257
|
// flow during a turn. We ignore the fine-grained stream_event partials in
|
|
243
258
|
// the loop; only the coarse thinking_tokens system message is surfaced.
|